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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +0 -22
  2. package/dist/devtools/index.html +68 -0
  3. package/dist/src/binder.d.ts +2 -0
  4. package/dist/src/binder.d.ts.map +1 -1
  5. package/dist/src/binder.js +55 -12
  6. package/dist/src/binder.js.map +1 -1
  7. package/dist/src/components/AppendFile.d.ts.map +1 -1
  8. package/dist/src/components/AppendFile.js +14 -3
  9. package/dist/src/components/AppendFile.js.map +1 -1
  10. package/dist/src/components/Block.js +1 -1
  11. package/dist/src/components/Block.js.map +1 -1
  12. package/dist/src/components/Declaration.d.ts.map +1 -1
  13. package/dist/src/components/Declaration.js +2 -1
  14. package/dist/src/components/Declaration.js.map +1 -1
  15. package/dist/src/components/Scope.d.ts.map +1 -1
  16. package/dist/src/components/Scope.js +4 -1
  17. package/dist/src/components/Scope.js.map +1 -1
  18. package/dist/src/components/TemplateFile.d.ts.map +1 -1
  19. package/dist/src/components/TemplateFile.js +18 -3
  20. package/dist/src/components/TemplateFile.js.map +1 -1
  21. package/dist/src/content-slot.d.ts.map +1 -1
  22. package/dist/src/content-slot.js +6 -5
  23. package/dist/src/content-slot.js.map +1 -1
  24. package/dist/src/context.d.ts.map +1 -1
  25. package/dist/src/context.js +8 -1
  26. package/dist/src/context.js.map +1 -1
  27. package/dist/src/debug/cli.d.ts +6 -0
  28. package/dist/src/debug/cli.d.ts.map +1 -0
  29. package/dist/src/{debug.js → debug/cli.js} +78 -84
  30. package/dist/src/debug/cli.js.map +1 -0
  31. package/dist/src/debug/diagnostics.test.d.ts +2 -0
  32. package/dist/src/debug/diagnostics.test.d.ts.map +1 -0
  33. package/dist/src/debug/diagnostics.test.js +45 -0
  34. package/dist/src/debug/diagnostics.test.js.map +1 -0
  35. package/dist/src/debug/effects.d.ts +69 -0
  36. package/dist/src/debug/effects.d.ts.map +1 -0
  37. package/dist/src/debug/effects.js +228 -0
  38. package/dist/src/debug/effects.js.map +1 -0
  39. package/dist/src/debug/effects.test.d.ts +2 -0
  40. package/dist/src/debug/effects.test.d.ts.map +1 -0
  41. package/dist/src/debug/effects.test.js +86 -0
  42. package/dist/src/debug/effects.test.js.map +1 -0
  43. package/dist/src/debug/files.d.ts +14 -0
  44. package/dist/src/debug/files.d.ts.map +1 -0
  45. package/dist/src/debug/files.js +40 -0
  46. package/dist/src/debug/files.js.map +1 -0
  47. package/dist/src/debug/files.test.d.ts +2 -0
  48. package/dist/src/debug/files.test.d.ts.map +1 -0
  49. package/dist/src/debug/files.test.js +89 -0
  50. package/dist/src/debug/files.test.js.map +1 -0
  51. package/dist/src/debug/index.d.ts +60 -0
  52. package/dist/src/debug/index.d.ts.map +1 -0
  53. package/dist/src/debug/index.js +68 -0
  54. package/dist/src/debug/index.js.map +1 -0
  55. package/dist/src/debug/render.d.ts +57 -0
  56. package/dist/src/debug/render.d.ts.map +1 -0
  57. package/dist/src/debug/render.js +519 -0
  58. package/dist/src/debug/render.js.map +1 -0
  59. package/dist/src/debug/render.test.d.ts +2 -0
  60. package/dist/src/debug/render.test.d.ts.map +1 -0
  61. package/dist/src/debug/render.test.js +328 -0
  62. package/dist/src/debug/render.test.js.map +1 -0
  63. package/dist/src/debug/serialize.d.ts +9 -0
  64. package/dist/src/debug/serialize.d.ts.map +1 -0
  65. package/dist/src/debug/serialize.js +70 -0
  66. package/dist/src/debug/serialize.js.map +1 -0
  67. package/dist/src/debug/symbols.d.ts +9 -0
  68. package/dist/src/debug/symbols.d.ts.map +1 -0
  69. package/dist/src/debug/symbols.js +164 -0
  70. package/dist/src/debug/symbols.js.map +1 -0
  71. package/dist/src/debug/symbols.test.d.ts +2 -0
  72. package/dist/src/debug/symbols.test.d.ts.map +1 -0
  73. package/dist/src/debug/symbols.test.js +104 -0
  74. package/dist/src/debug/symbols.test.js.map +1 -0
  75. package/dist/src/debug/trace.d.ts +342 -0
  76. package/dist/src/debug/trace.d.ts.map +1 -0
  77. package/dist/src/debug/trace.js +443 -0
  78. package/dist/src/debug/trace.js.map +1 -0
  79. package/dist/src/devtools/devtools-protocol.d.ts +232 -0
  80. package/dist/src/devtools/devtools-protocol.d.ts.map +1 -0
  81. package/dist/src/devtools/devtools-protocol.js +2 -0
  82. package/dist/src/devtools/devtools-protocol.js.map +1 -0
  83. package/dist/src/devtools/devtools-server.browser.d.ts +28 -0
  84. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -0
  85. package/dist/src/devtools/devtools-server.browser.js +36 -0
  86. package/dist/src/devtools/devtools-server.browser.js.map +1 -0
  87. package/dist/src/devtools/devtools-server.d.ts +72 -0
  88. package/dist/src/devtools/devtools-server.d.ts.map +1 -0
  89. package/dist/src/devtools/devtools-server.js +256 -0
  90. package/dist/src/devtools/devtools-server.js.map +1 -0
  91. package/dist/src/devtools/devtools-transport.d.ts +23 -0
  92. package/dist/src/devtools/devtools-transport.d.ts.map +1 -0
  93. package/dist/src/devtools/devtools-transport.js +114 -0
  94. package/dist/src/devtools/devtools-transport.js.map +1 -0
  95. package/dist/src/devtools-entry.browser.d.ts +4 -0
  96. package/dist/src/devtools-entry.browser.d.ts.map +1 -0
  97. package/dist/src/devtools-entry.browser.js +2 -0
  98. package/dist/src/devtools-entry.browser.js.map +1 -0
  99. package/dist/src/devtools-entry.d.ts +4 -0
  100. package/dist/src/devtools-entry.d.ts.map +1 -0
  101. package/dist/src/devtools-entry.js +2 -0
  102. package/dist/src/devtools-entry.js.map +1 -0
  103. package/dist/src/diagnostics.d.ts +34 -0
  104. package/dist/src/diagnostics.d.ts.map +1 -0
  105. package/dist/src/diagnostics.js +89 -0
  106. package/dist/src/diagnostics.js.map +1 -0
  107. package/dist/src/index.d.ts +3 -2
  108. package/dist/src/index.d.ts.map +1 -1
  109. package/dist/src/index.js +3 -2
  110. package/dist/src/index.js.map +1 -1
  111. package/dist/src/print-hook.d.ts +14 -0
  112. package/dist/src/print-hook.d.ts.map +1 -0
  113. package/dist/src/print-hook.js +10 -0
  114. package/dist/src/print-hook.js.map +1 -0
  115. package/dist/src/reactive-union-set.d.ts.map +1 -1
  116. package/dist/src/reactive-union-set.js +15 -0
  117. package/dist/src/reactive-union-set.js.map +1 -1
  118. package/dist/src/reactivity.d.ts +17 -3
  119. package/dist/src/reactivity.d.ts.map +1 -1
  120. package/dist/src/reactivity.js +162 -14
  121. package/dist/src/reactivity.js.map +1 -1
  122. package/dist/src/render-stack.d.ts +29 -0
  123. package/dist/src/render-stack.d.ts.map +1 -0
  124. package/dist/src/render-stack.js +247 -0
  125. package/dist/src/render-stack.js.map +1 -0
  126. package/dist/src/render.d.ts +9 -19
  127. package/dist/src/render.d.ts.map +1 -1
  128. package/dist/src/render.js +363 -153
  129. package/dist/src/render.js.map +1 -1
  130. package/dist/src/resource.d.ts.map +1 -1
  131. package/dist/src/resource.js +5 -0
  132. package/dist/src/resource.js.map +1 -1
  133. package/dist/src/runtime/component.d.ts +7 -1
  134. package/dist/src/runtime/component.d.ts.map +1 -1
  135. package/dist/src/runtime/component.js +4 -1
  136. package/dist/src/runtime/component.js.map +1 -1
  137. package/dist/src/scheduler.d.ts +3 -0
  138. package/dist/src/scheduler.d.ts.map +1 -1
  139. package/dist/src/scheduler.js +45 -2
  140. package/dist/src/scheduler.js.map +1 -1
  141. package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
  142. package/dist/src/symbols/basic-symbol.js +6 -1
  143. package/dist/src/symbols/basic-symbol.js.map +1 -1
  144. package/dist/src/symbols/decl.d.ts.map +1 -1
  145. package/dist/src/symbols/decl.js +5 -1
  146. package/dist/src/symbols/decl.js.map +1 -1
  147. package/dist/src/symbols/output-scope.d.ts +2 -1
  148. package/dist/src/symbols/output-scope.d.ts.map +1 -1
  149. package/dist/src/symbols/output-scope.js +13 -8
  150. package/dist/src/symbols/output-scope.js.map +1 -1
  151. package/dist/src/symbols/output-symbol.d.ts +1 -0
  152. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  153. package/dist/src/symbols/output-symbol.js +23 -6
  154. package/dist/src/symbols/output-symbol.js.map +1 -1
  155. package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
  156. package/dist/src/symbols/symbol-flow.js +22 -6
  157. package/dist/src/symbols/symbol-flow.js.map +1 -1
  158. package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
  159. package/dist/src/symbols/symbol-slot.js +15 -0
  160. package/dist/src/symbols/symbol-slot.js.map +1 -1
  161. package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
  162. package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
  163. package/dist/src/symbols/symbol-slot.test.js +35 -0
  164. package/dist/src/symbols/symbol-slot.test.js.map +1 -0
  165. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  166. package/dist/src/symbols/symbol-table.js +6 -5
  167. package/dist/src/symbols/symbol-table.js.map +1 -1
  168. package/dist/src/trace.d.ts +2 -0
  169. package/dist/src/trace.d.ts.map +1 -0
  170. package/dist/src/trace.js +2 -0
  171. package/dist/src/trace.js.map +1 -0
  172. package/dist/src/tracer.d.ts +2 -228
  173. package/dist/src/tracer.d.ts.map +1 -1
  174. package/dist/src/tracer.js +5 -298
  175. package/dist/src/tracer.js.map +1 -1
  176. package/dist/src/utils.d.ts.map +1 -1
  177. package/dist/src/utils.js +5 -0
  178. package/dist/src/utils.js.map +1 -1
  179. package/dist/test/components/append-file.test.d.ts.map +1 -1
  180. package/dist/test/components/append-file.test.js +18 -10
  181. package/dist/test/components/append-file.test.js.map +1 -1
  182. package/dist/test/components/template-file.test.d.ts.map +1 -1
  183. package/dist/test/components/template-file.test.js +6 -4
  184. package/dist/test/components/template-file.test.js.map +1 -1
  185. package/dist/test/rendering/basic.test.js +3 -0
  186. package/dist/test/rendering/basic.test.js.map +1 -1
  187. package/dist/test/rendering/print-render-stack.test.d.ts +2 -0
  188. package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -0
  189. package/dist/test/rendering/print-render-stack.test.js +207 -0
  190. package/dist/test/rendering/print-render-stack.test.js.map +1 -0
  191. package/dist/testing/create-test-wrapper.d.ts +1 -1
  192. package/dist/testing/create-test-wrapper.d.ts.map +1 -1
  193. package/dist/testing/create-test-wrapper.js +1 -1
  194. package/dist/testing/create-test-wrapper.js.map +1 -1
  195. package/dist/testing/devtools-utils.d.ts +26 -0
  196. package/dist/testing/devtools-utils.d.ts.map +1 -0
  197. package/dist/testing/devtools-utils.js +140 -0
  198. package/dist/testing/devtools-utils.js.map +1 -0
  199. package/dist/testing/extend-expect.d.ts.map +1 -1
  200. package/dist/testing/extend-expect.js +63 -1
  201. package/dist/testing/extend-expect.js.map +1 -1
  202. package/dist/testing/render.d.ts +2 -2
  203. package/dist/testing/render.d.ts.map +1 -1
  204. package/dist/testing/render.js +2 -2
  205. package/dist/testing/render.js.map +1 -1
  206. package/dist/tsconfig.tsbuildinfo +1 -1
  207. package/package.json +21 -7
  208. package/scripts/copy-devtools-ui.mjs +26 -0
  209. package/src/binder.ts +71 -16
  210. package/src/components/AppendFile.tsx +14 -9
  211. package/src/components/Block.tsx +1 -1
  212. package/src/components/Declaration.tsx +2 -1
  213. package/src/components/Scope.tsx +4 -1
  214. package/src/components/TemplateFile.tsx +18 -9
  215. package/src/content-slot.tsx +6 -6
  216. package/src/context.ts +15 -4
  217. package/src/{debug.ts → debug/cli.ts} +112 -127
  218. package/src/debug/diagnostics.test.tsx +55 -0
  219. package/src/debug/effects.test.tsx +96 -0
  220. package/src/debug/effects.ts +313 -0
  221. package/src/debug/files.test.tsx +96 -0
  222. package/src/debug/files.ts +40 -0
  223. package/src/debug/index.ts +126 -0
  224. package/src/debug/render.test.tsx +379 -0
  225. package/src/debug/render.ts +639 -0
  226. package/src/debug/serialize.ts +85 -0
  227. package/src/debug/symbols.test.tsx +106 -0
  228. package/src/debug/symbols.ts +230 -0
  229. package/src/debug/trace.ts +312 -0
  230. package/src/devtools/devtools-protocol.ts +312 -0
  231. package/src/devtools/devtools-server.browser.ts +71 -0
  232. package/src/devtools/devtools-server.ts +290 -0
  233. package/src/devtools/devtools-transport.ts +154 -0
  234. package/src/devtools-entry.browser.ts +52 -0
  235. package/src/devtools-entry.ts +54 -0
  236. package/src/diagnostics.ts +141 -0
  237. package/src/index.ts +2 -6
  238. package/src/print-hook.ts +22 -0
  239. package/src/reactive-union-set.ts +71 -41
  240. package/src/reactivity.ts +206 -23
  241. package/src/render-stack.ts +289 -0
  242. package/src/render.ts +464 -212
  243. package/src/resource.ts +28 -19
  244. package/src/runtime/component.ts +11 -0
  245. package/src/scheduler.ts +55 -3
  246. package/src/symbols/basic-symbol.ts +6 -1
  247. package/src/symbols/decl.ts +5 -1
  248. package/src/symbols/output-scope.ts +21 -12
  249. package/src/symbols/output-symbol.ts +33 -12
  250. package/src/symbols/symbol-flow.ts +68 -37
  251. package/src/symbols/symbol-slot.test.tsx +41 -0
  252. package/src/symbols/symbol-slot.tsx +47 -20
  253. package/src/symbols/symbol-table.ts +6 -10
  254. package/src/trace.ts +1 -0
  255. package/src/tracer.ts +13 -242
  256. package/src/utils.tsx +22 -13
  257. package/temp/api.json +1811 -277
  258. package/test/components/append-file.test.tsx +36 -29
  259. package/test/components/template-file.test.tsx +11 -11
  260. package/test/rendering/basic.test.tsx +4 -0
  261. package/test/rendering/print-render-stack.test.tsx +244 -0
  262. package/testing/create-test-wrapper.tsx +1 -1
  263. package/testing/devtools-utils.ts +203 -0
  264. package/testing/extend-expect.ts +89 -0
  265. package/testing/render.ts +2 -2
  266. package/testing/vitest.d.ts +9 -0
  267. package/dist/src/debug.d.ts +0 -15
  268. package/dist/src/debug.d.ts.map +0 -1
  269. package/dist/src/debug.js.map +0 -1
@@ -0,0 +1,639 @@
1
+ import { watch } from "@vue/reactivity";
2
+ import * as devalue from "devalue";
3
+ import type {
4
+ RenderErrorStackEntry as ProtocolRenderErrorStackEntry,
5
+ RenderTreeNode,
6
+ } from "../devtools/devtools-protocol.js";
7
+ import {
8
+ isDevtoolsEnabled,
9
+ registerDevtoolsMessageHandler,
10
+ } from "../devtools/devtools-server.js";
11
+ import {
12
+ isPrintHook,
13
+ type PrintHook,
14
+ type RenderedTextTree,
15
+ } from "../print-hook.js";
16
+ import { untrack } from "../reactivity.js";
17
+ import type { ComponentCreator } from "../runtime/component.js";
18
+ import { flushJobsAsync } from "../scheduler.js";
19
+ import { sanitizeRecord } from "./serialize.js";
20
+ import { emitDevtoolsMessage } from "./trace.js";
21
+
22
+ /** The kind discriminant for render tree nodes. */
23
+ export type RenderTreeNodeKind = RenderTreeNode["kind"];
24
+
25
+ /** Information about a render tree node, used when recording nodes to devtools. */
26
+ export interface RenderTreeNodeInfo {
27
+ kind: RenderTreeNodeKind;
28
+ name?: string;
29
+ propsSerialized?: string;
30
+ value?: string;
31
+ source?: RenderTreeNode["source"];
32
+ }
33
+
34
+ export interface RenderNodeActions {
35
+ rerender: () => void;
36
+ rerenderAndBreak: () => void;
37
+ }
38
+
39
+ export interface BeginComponentOptions {
40
+ parent: RenderedTextTree;
41
+ index: number;
42
+ node: RenderedTextTree;
43
+ component: ComponentCreator<unknown>;
44
+ propsSource: Record<string, unknown> | undefined;
45
+ source: RenderTreeNodeInfo["source"] | undefined;
46
+ isExisting: boolean;
47
+ actions: RenderNodeActions;
48
+ }
49
+
50
+ export interface ComponentDebugSession {
51
+ recordDirectory(path: string): void;
52
+ recordFile(path: string, filetype: string): void;
53
+ dispose(): void;
54
+ }
55
+
56
+ /** Any node tracked by the devtools render tree. */
57
+ type TrackedNode = RenderedTextTree | PrintHook;
58
+
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+ // Module state — reset in initialize()
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+
63
+ let nodeIds = new WeakMap<TrackedNode, number>();
64
+ let idToNode = new Map<number, TrackedNode>();
65
+ let entryIds = new WeakMap<RenderedTextTree, number[]>();
66
+ let fileNodes = new Map<number, { path: string; filetype: string }>();
67
+ let directoryNodes = new Map<number, { path: string }>();
68
+ let nodeProps = new Map<number, string | undefined>();
69
+ let rerenderActions = new Map<number, RenderNodeActions>();
70
+ let nextId = 1;
71
+ let handlerRegistered = false;
72
+
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ // Props serialization
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+
77
+ function serializeRenderTreeProps(input: Record<string, unknown> | undefined) {
78
+ return untrack(() => {
79
+ if (!input) return undefined;
80
+ const { children: _children, ...rest } = input;
81
+ const sanitized = sanitizeRecord(rest);
82
+ if (!sanitized) return undefined;
83
+ try {
84
+ return devalue.stringify(sanitized);
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ });
89
+ }
90
+
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ // Node ID management & tree structure
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ function emitNodeRemoved(parentId: number | null, id: number) {
96
+ clearRenderTreeChildrenForId(id);
97
+ emitDevtoolsMessage({
98
+ type: "render:nodeRemoved",
99
+ parentId,
100
+ id,
101
+ });
102
+
103
+ rerenderActions.delete(id);
104
+ nodeProps.delete(id);
105
+ idToNode.delete(id);
106
+
107
+ const fileInfo = fileNodes.get(id);
108
+ if (fileInfo) {
109
+ emitDevtoolsMessage({
110
+ type: "files:fileRemoved",
111
+ path: fileInfo.path,
112
+ });
113
+ fileNodes.delete(id);
114
+ }
115
+
116
+ const dirInfo = directoryNodes.get(id);
117
+ if (dirInfo) {
118
+ emitDevtoolsMessage({
119
+ type: "files:directoryRemoved",
120
+ path: dirInfo.path,
121
+ });
122
+ directoryNodes.delete(id);
123
+ }
124
+ }
125
+
126
+ function getEntryList(parent: RenderedTextTree) {
127
+ let list = entryIds.get(parent);
128
+ if (!list) {
129
+ list = [];
130
+ entryIds.set(parent, list);
131
+ }
132
+ return list;
133
+ }
134
+
135
+ function getOrCreateNodeId(node: TrackedNode) {
136
+ const existing = nodeIds.get(node);
137
+ if (existing) return existing;
138
+ const id = nextId++;
139
+ nodeIds.set(node, id);
140
+ idToNode.set(id, node);
141
+ return id;
142
+ }
143
+
144
+ export function getRenderNodeId(node: RenderedTextTree | PrintHook) {
145
+ if (!isDevtoolsEnabled()) return undefined;
146
+ return getOrCreateNodeId(node);
147
+ }
148
+
149
+ function setEntryId(parent: RenderedTextTree, index: number, id: number) {
150
+ const list = getEntryList(parent);
151
+ list[index] = id;
152
+ }
153
+
154
+ export function initialize(root: RenderedTextTree) {
155
+ if (!isDevtoolsEnabled()) return;
156
+ ensureDevtoolsHandler();
157
+ nodeIds = new WeakMap();
158
+ idToNode = new Map();
159
+ entryIds = new WeakMap();
160
+ fileNodes = new Map();
161
+ directoryNodes = new Map();
162
+ nodeProps = new Map();
163
+ rerenderActions = new Map();
164
+ nextId = 1;
165
+ emitDevtoolsMessage({ type: "render:reset" });
166
+ const rootId = getOrCreateNodeId(root);
167
+ emitDevtoolsMessage({
168
+ type: "render:nodeAdded",
169
+ parentId: null,
170
+ node: {
171
+ id: rootId,
172
+ kind: "root",
173
+ },
174
+ });
175
+ }
176
+
177
+ export function registerRenderNodeActions(
178
+ node: RenderedTextTree | PrintHook,
179
+ actions: RenderNodeActions,
180
+ ) {
181
+ if (!isDevtoolsEnabled()) return;
182
+ const id = getOrCreateNodeId(node);
183
+ rerenderActions.set(id, actions);
184
+ }
185
+
186
+ export function unregisterRenderNodeActions(
187
+ node: RenderedTextTree | PrintHook,
188
+ ) {
189
+ if (!isDevtoolsEnabled()) return;
190
+ const id = nodeIds.get(node);
191
+ if (id !== undefined) {
192
+ rerenderActions.delete(id);
193
+ }
194
+ }
195
+
196
+ function ensureDevtoolsHandler() {
197
+ if (handlerRegistered || !isDevtoolsEnabled()) return;
198
+ handlerRegistered = true;
199
+ registerDevtoolsMessageHandler((message) => {
200
+ if (
201
+ message.type !== "render:rerender" &&
202
+ message.type !== "render:rerenderAndBreak"
203
+ ) {
204
+ return;
205
+ }
206
+ const rawId = (message as { id?: unknown }).id;
207
+ if (typeof rawId !== "number" && typeof rawId !== "string") return;
208
+ const id = Number(rawId);
209
+ if (!Number.isFinite(id)) return;
210
+ const actions = rerenderActions.get(id);
211
+ if (!actions) return;
212
+ if (message.type === "render:rerender") {
213
+ actions.rerender();
214
+ } else {
215
+ actions.rerenderAndBreak();
216
+ }
217
+ void flushJobsAsync();
218
+ });
219
+ }
220
+
221
+ export function recordTextNode(
222
+ parent: RenderedTextTree,
223
+ index: number,
224
+ value: string,
225
+ ) {
226
+ if (!isDevtoolsEnabled()) return;
227
+ const id = nextId++;
228
+ setEntryId(parent, index, id);
229
+ emitDevtoolsMessage({
230
+ type: "render:nodeAdded",
231
+ parentId: getOrCreateNodeId(parent),
232
+ node: {
233
+ id,
234
+ kind: "text",
235
+ value,
236
+ },
237
+ });
238
+ }
239
+
240
+ function recordNodeAdded(
241
+ parent: RenderedTextTree,
242
+ index: number,
243
+ node: RenderedTextTree | PrintHook,
244
+ info: RenderTreeNodeInfo,
245
+ ) {
246
+ if (!isDevtoolsEnabled()) return;
247
+ const id = getOrCreateNodeId(node);
248
+ if (info.propsSerialized !== undefined) {
249
+ nodeProps.set(id, info.propsSerialized);
250
+ }
251
+ setEntryId(parent, index, id);
252
+ emitDevtoolsMessage({
253
+ type: "render:nodeAdded",
254
+ parentId: getOrCreateNodeId(parent),
255
+ node: {
256
+ id,
257
+ ...info,
258
+ },
259
+ });
260
+ }
261
+
262
+ function recordSubtreeAdded(
263
+ parentNode: RenderedTextTree | PrintHook,
264
+ subtree: RenderedTextTree,
265
+ info: RenderTreeNodeInfo = { kind: "fragment" },
266
+ ) {
267
+ if (!isDevtoolsEnabled()) return;
268
+ const parentId = getOrCreateNodeId(parentNode);
269
+ // Check if this node was previously rendered (cached) by seeing if it already has an ID
270
+ const existingId = nodeIds.get(subtree);
271
+ const isCached = existingId !== undefined;
272
+ const id = isCached ? existingId : getOrCreateNodeId(subtree);
273
+ // Track in entryIds so clearRenderTreeChildren can find and remove it
274
+ if (Array.isArray(parentNode)) {
275
+ const list = getEntryList(parentNode);
276
+ list.push(id);
277
+ }
278
+ emitDevtoolsMessage({
279
+ type: "render:nodeAdded",
280
+ parentId,
281
+ node: {
282
+ id,
283
+ ...info,
284
+ },
285
+ });
286
+ // For cached nodes, we need to recursively re-add all their children since
287
+ // clearRenderTreeChildren removed them when the parent re-rendered
288
+ if (isCached) {
289
+ recordCachedSubtreeChildrenRecursively(subtree);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Recursively re-adds all children of a cached render tree node to devtools.
295
+ * This is needed because clearRenderTreeChildren recursively removes all
296
+ * descendants, but cached nodes aren't re-rendered so their children need
297
+ * to be explicitly re-added.
298
+ */
299
+ function recordCachedSubtreeChildrenRecursively(node: RenderedTextTree) {
300
+ if (!isDevtoolsEnabled()) return;
301
+ const parentId = getOrCreateNodeId(node);
302
+
303
+ // Rebuild the entryIds for this node
304
+ const list = getEntryList(node);
305
+ list.length = 0;
306
+
307
+ for (let i = 0; i < node.length; i++) {
308
+ const child = node[i];
309
+ if (typeof child === "string") {
310
+ // Text nodes - re-record them with new IDs
311
+ if (child !== "") {
312
+ const id = nextId++;
313
+ list.push(id);
314
+ idToNode.set(id, child as unknown as RenderedTextTree);
315
+ emitDevtoolsMessage({
316
+ type: "render:nodeAdded",
317
+ parentId,
318
+ node: {
319
+ id,
320
+ kind: "text",
321
+ value: child,
322
+ },
323
+ });
324
+ }
325
+ } else if (Array.isArray(child)) {
326
+ // Nested RenderedTextTree - record and recurse
327
+ const id = getOrCreateNodeId(child);
328
+ list.push(id);
329
+ emitDevtoolsMessage({
330
+ type: "render:nodeAdded",
331
+ parentId,
332
+ node: {
333
+ id,
334
+ kind: "fragment",
335
+ },
336
+ });
337
+ recordCachedSubtreeChildrenRecursively(child);
338
+ } else if (isPrintHook(child)) {
339
+ // PrintHook - record and recurse into subtree
340
+ const id = getOrCreateNodeId(child);
341
+ list.push(id);
342
+ emitDevtoolsMessage({
343
+ type: "render:nodeAdded",
344
+ parentId,
345
+ node: {
346
+ id,
347
+ kind: "printHook",
348
+ name: (child as { name?: string }).name ?? "hook",
349
+ },
350
+ });
351
+ if (child.subtree) {
352
+ const subtreeId = getOrCreateNodeId(child.subtree);
353
+ const hookList = getEntryList(child as unknown as RenderedTextTree);
354
+ hookList.length = 0;
355
+ hookList.push(subtreeId);
356
+ emitDevtoolsMessage({
357
+ type: "render:nodeAdded",
358
+ parentId: id,
359
+ node: {
360
+ id: subtreeId,
361
+ kind: "fragment",
362
+ },
363
+ });
364
+ recordCachedSubtreeChildrenRecursively(child.subtree);
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ function recordNodePropsUpdated(
371
+ node: RenderedTextTree | PrintHook,
372
+ propsSerialized: string | undefined,
373
+ ) {
374
+ if (!isDevtoolsEnabled()) return;
375
+ const id = getOrCreateNodeId(node);
376
+ const previous = nodeProps.get(id);
377
+ if (previous === propsSerialized) return;
378
+ nodeProps.set(id, propsSerialized);
379
+ emitDevtoolsMessage({
380
+ type: "render:nodeUpdated",
381
+ id,
382
+ propsSerialized,
383
+ });
384
+ }
385
+
386
+ function clearRenderTreeChildren(parent: RenderedTextTree) {
387
+ if (!isDevtoolsEnabled()) return;
388
+ const list = entryIds.get(parent);
389
+ if (!list || list.length === 0) return;
390
+ const parentId = getOrCreateNodeId(parent);
391
+ for (const id of list) {
392
+ if (id !== undefined) {
393
+ emitNodeRemoved(parentId, id);
394
+ }
395
+ }
396
+ entryIds.set(parent, []);
397
+ }
398
+
399
+ function clearRenderTreeChildrenForId(id: number) {
400
+ const node = idToNode.get(id);
401
+ if (!node) return;
402
+ if (Array.isArray(node)) {
403
+ clearRenderTreeChildren(node);
404
+ return;
405
+ }
406
+ const subtree = (node as { subtree?: RenderedTextTree }).subtree;
407
+ if (subtree && Array.isArray(subtree)) {
408
+ clearRenderTreeChildren(subtree);
409
+ }
410
+ }
411
+
412
+ // ─────────────────────────────────────────────────────────────────────────────
413
+ // File / directory node tracking
414
+ // ─────────────────────────────────────────────────────────────────────────────
415
+
416
+ function recordDirectoryNode(node: RenderedTextTree, path: string) {
417
+ if (!isDevtoolsEnabled()) return;
418
+ const id = getOrCreateNodeId(node);
419
+ if (directoryNodes.has(id)) return;
420
+ directoryNodes.set(id, { path });
421
+ emitDevtoolsMessage({
422
+ type: "files:directoryAdded",
423
+ path,
424
+ });
425
+ }
426
+
427
+ function recordFileNode(
428
+ node: RenderedTextTree,
429
+ path: string,
430
+ filetype: string,
431
+ ) {
432
+ if (!isDevtoolsEnabled()) return;
433
+ const id = getOrCreateNodeId(node);
434
+ if (fileNodes.has(id)) return;
435
+ fileNodes.set(id, { path, filetype });
436
+ emitDevtoolsMessage({
437
+ type: "files:fileAdded",
438
+ path,
439
+ filetype,
440
+ renderNodeId: id,
441
+ });
442
+ }
443
+
444
+ function removeFileEntriesForNode(node: RenderedTextTree | PrintHook) {
445
+ if (!isDevtoolsEnabled()) return;
446
+ const id = nodeIds.get(node);
447
+ if (id === undefined) return;
448
+ const fileInfo = fileNodes.get(id);
449
+ if (fileInfo) {
450
+ emitDevtoolsMessage({
451
+ type: "files:fileRemoved",
452
+ path: fileInfo.path,
453
+ });
454
+ fileNodes.delete(id);
455
+ }
456
+ const dirInfo = directoryNodes.get(id);
457
+ if (dirInfo) {
458
+ emitDevtoolsMessage({
459
+ type: "files:directoryRemoved",
460
+ path: dirInfo.path,
461
+ });
462
+ directoryNodes.delete(id);
463
+ }
464
+ }
465
+
466
+ // ─────────────────────────────────────────────────────────────────────────────
467
+ // Public API — called from render.ts via the debug object
468
+ // ─────────────────────────────────────────────────────────────────────────────
469
+
470
+ /** Begin tracking a component render. Returns a session to record files/dirs and dispose watchers. */
471
+ export function beginComponent(
472
+ options: BeginComponentOptions,
473
+ ): ComponentDebugSession {
474
+ const {
475
+ parent,
476
+ index,
477
+ node,
478
+ component,
479
+ propsSource,
480
+ source,
481
+ isExisting,
482
+ actions,
483
+ } = options;
484
+
485
+ if (!isDevtoolsEnabled()) {
486
+ return {
487
+ recordDirectory() {},
488
+ recordFile() {},
489
+ dispose() {},
490
+ };
491
+ }
492
+
493
+ return untrack(() => {
494
+ let componentName = component.component.name;
495
+ if (componentName === "Provider") {
496
+ const contextName = (component.component as any).contextName as
497
+ | string
498
+ | undefined;
499
+ if (contextName) {
500
+ componentName = `Context ${contextName}`;
501
+ }
502
+ }
503
+ const propsSerialized = serializeRenderTreeProps(propsSource);
504
+ if (isExisting) {
505
+ clearRenderTreeChildren(node);
506
+ } else {
507
+ recordNodeAdded(parent, index, node, {
508
+ kind: "component",
509
+ name: componentName,
510
+ propsSerialized,
511
+ source,
512
+ });
513
+ }
514
+ recordNodePropsUpdated(node, propsSerialized);
515
+ registerRenderNodeActions(node, actions);
516
+
517
+ let stopWatch: (() => void) | undefined;
518
+ if (propsSource) {
519
+ const propKeys = Object.keys(propsSource).filter(
520
+ (key) => key !== "children",
521
+ );
522
+ if (propKeys.length > 0) {
523
+ stopWatch = watch(
524
+ () => propKeys.map((key) => propsSource[key]),
525
+ () => {
526
+ const nextSerialized = serializeRenderTreeProps(propsSource);
527
+ recordNodePropsUpdated(node, nextSerialized);
528
+ },
529
+ );
530
+ }
531
+ }
532
+
533
+ return {
534
+ recordDirectory(path: string) {
535
+ recordDirectoryNode(node, path);
536
+ },
537
+ recordFile(path: string, filetype: string) {
538
+ recordFileNode(node, path, filetype);
539
+ },
540
+ dispose() {
541
+ stopWatch?.();
542
+ removeFileEntriesForNode(node);
543
+ unregisterRenderNodeActions(node);
544
+ },
545
+ };
546
+ });
547
+ }
548
+
549
+ export function appendCustomContext(
550
+ parent: RenderedTextTree,
551
+ node: RenderedTextTree,
552
+ ) {
553
+ recordSubtreeAdded(parent, node, { kind: "customContext" });
554
+ }
555
+
556
+ export function appendPrintHook(
557
+ parent: RenderedTextTree,
558
+ index: number,
559
+ hook: PrintHook,
560
+ name: string,
561
+ subtree?: RenderedTextTree,
562
+ ) {
563
+ recordNodeAdded(parent, index, hook, { kind: "printHook", name });
564
+ if (subtree) {
565
+ recordSubtreeAdded(hook, subtree);
566
+ }
567
+ }
568
+
569
+ export function appendFragmentChild(
570
+ parent: RenderedTextTree,
571
+ child: RenderedTextTree,
572
+ ) {
573
+ recordSubtreeAdded(parent, child, { kind: "fragment" });
574
+ }
575
+
576
+ export function appendTextNode(
577
+ parent: RenderedTextTree,
578
+ index: number,
579
+ value: string,
580
+ ) {
581
+ recordTextNode(parent, index, value);
582
+ }
583
+
584
+ export function prepareMemoNode(
585
+ parent: RenderedTextTree,
586
+ node: RenderedTextTree,
587
+ isExisting: boolean,
588
+ ) {
589
+ if (isExisting) {
590
+ clearRenderTreeChildren(node);
591
+ return;
592
+ }
593
+ recordSubtreeAdded(parent, node, { kind: "memo" });
594
+ }
595
+
596
+ let nextErrorId = 1;
597
+
598
+ export interface RenderErrorInfo {
599
+ name: string;
600
+ message: string;
601
+ stack?: string;
602
+ }
603
+
604
+ /** Render error stack entry with optional runtime props (extends protocol type). */
605
+ export interface RenderErrorStackEntry extends ProtocolRenderErrorStackEntry {
606
+ props?: Record<string, unknown> | undefined;
607
+ }
608
+
609
+ export function error(
610
+ error: RenderErrorInfo,
611
+ componentStack: RenderErrorStackEntry[],
612
+ ) {
613
+ if (!isDevtoolsEnabled()) return;
614
+ const serializedStack = untrack(() =>
615
+ componentStack.map((entry) => ({
616
+ name: entry.name,
617
+ propsSerialized: entry.propsSerialized,
618
+ renderNodeId: entry.renderNodeId,
619
+ source: entry.source,
620
+ })),
621
+ );
622
+ const message = {
623
+ type: "render:error" as const,
624
+ id: nextErrorId++,
625
+ name: error.name,
626
+ message: error.message,
627
+ stack: error.stack,
628
+ componentStack: serializedStack,
629
+ };
630
+ emitDevtoolsMessage(message);
631
+ }
632
+
633
+ export function complete() {
634
+ emitDevtoolsMessage({ type: "render:complete" });
635
+ }
636
+
637
+ export function flushJobsComplete() {
638
+ emitDevtoolsMessage({ type: "flushJobs:complete" });
639
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared serialization utilities for debug modules.
3
+ *
4
+ * Provides safe, depth-limited serialization of arbitrary values for
5
+ * transmission over the devtools protocol. Handles reactive refs, circular
6
+ * references, and non-plain objects.
7
+ */
8
+ import { isReactive, isRef } from "@vue/reactivity";
9
+ import { untrack } from "../reactivity.js";
10
+
11
+ const MAX_ENTRIES = 50;
12
+ const MAX_DEPTH = 3;
13
+
14
+ function isPlainObject(value: unknown): boolean {
15
+ if (!value || typeof value !== "object") return false;
16
+ const proto = Object.getPrototypeOf(value);
17
+ return proto === Object.prototype || proto === null;
18
+ }
19
+
20
+ function sanitize(
21
+ value: unknown,
22
+ depth: number,
23
+ seen: WeakSet<object>,
24
+ ): unknown {
25
+ if (depth > MAX_DEPTH) return "[MaxDepth]";
26
+ if (
27
+ value === null ||
28
+ typeof value === "string" ||
29
+ typeof value === "number" ||
30
+ typeof value === "boolean"
31
+ ) {
32
+ return value;
33
+ }
34
+ if (typeof value === "bigint") return value.toString();
35
+ if (typeof value === "symbol") return value.toString();
36
+ if (typeof value === "function") return "[Function]";
37
+ if (isRef(value)) {
38
+ return sanitize(value.value, depth + 1, seen);
39
+ }
40
+ if (isReactive(value)) {
41
+ return "[Reactive]";
42
+ }
43
+ if (Array.isArray(value)) {
44
+ return value
45
+ .slice(0, MAX_ENTRIES)
46
+ .map((item) => sanitize(item, depth + 1, seen));
47
+ }
48
+ if (typeof value === "object") {
49
+ const obj = value as Record<string, unknown>;
50
+ if (seen.has(obj)) return "[Circular]";
51
+ seen.add(obj);
52
+ if (!isPlainObject(obj)) {
53
+ const name = obj.constructor?.name ?? "Object";
54
+ return `[${name}]`;
55
+ }
56
+ const entries = Object.entries(obj).slice(0, MAX_ENTRIES);
57
+ const result: Record<string, unknown> = {};
58
+ for (const [key, val] of entries) {
59
+ result[key] = sanitize(val, depth + 1, seen);
60
+ }
61
+ return result;
62
+ }
63
+ return String(value);
64
+ }
65
+
66
+ /**
67
+ * Sanitize a record of metadata for safe serialization over the devtools
68
+ * protocol. Unwraps refs, replaces reactive objects with placeholders, limits
69
+ * depth and entry count, and detects circular references.
70
+ *
71
+ * Runs inside `untrack()` to avoid creating reactive dependencies.
72
+ */
73
+ export function sanitizeRecord(
74
+ input: Record<string, unknown> | undefined,
75
+ ): Record<string, unknown> | undefined {
76
+ return untrack(() => {
77
+ if (!input) return undefined;
78
+ const seen = new WeakSet<object>();
79
+ const result: Record<string, unknown> = {};
80
+ for (const [key, value] of Object.entries(input).slice(0, MAX_ENTRIES)) {
81
+ result[key] = sanitize(value, 0, seen);
82
+ }
83
+ return result;
84
+ });
85
+ }