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

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 (310) 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/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/Prose.js +2 -2
  24. package/dist/src/components/Prose.js.map +1 -1
  25. package/dist/src/components/Scope.d.ts.map +1 -1
  26. package/dist/src/components/Scope.js +6 -1
  27. package/dist/src/components/Scope.js.map +1 -1
  28. package/dist/src/components/SourceDirectory.d.ts.map +1 -1
  29. package/dist/src/components/SourceDirectory.js +1 -2
  30. package/dist/src/components/SourceDirectory.js.map +1 -1
  31. package/dist/src/components/TemplateFile.d.ts.map +1 -1
  32. package/dist/src/components/TemplateFile.js +18 -3
  33. package/dist/src/components/TemplateFile.js.map +1 -1
  34. package/dist/src/components/index.d.ts +1 -0
  35. package/dist/src/components/index.d.ts.map +1 -1
  36. package/dist/src/components/index.js +1 -0
  37. package/dist/src/components/index.js.map +1 -1
  38. package/dist/src/content-slot.d.ts.map +1 -1
  39. package/dist/src/content-slot.js +7 -6
  40. package/dist/src/content-slot.js.map +1 -1
  41. package/dist/src/context.d.ts.map +1 -1
  42. package/dist/src/context.js +10 -3
  43. package/dist/src/context.js.map +1 -1
  44. package/dist/src/debug/cli.d.ts +6 -0
  45. package/dist/src/debug/cli.d.ts.map +1 -0
  46. package/dist/src/{debug.js → debug/cli.js} +79 -82
  47. package/dist/src/debug/cli.js.map +1 -0
  48. package/dist/src/debug/diagnostics.test.d.ts +2 -0
  49. package/dist/src/debug/diagnostics.test.d.ts.map +1 -0
  50. package/dist/src/debug/diagnostics.test.js +45 -0
  51. package/dist/src/debug/diagnostics.test.js.map +1 -0
  52. package/dist/src/debug/effects.d.ts +73 -0
  53. package/dist/src/debug/effects.d.ts.map +1 -0
  54. package/dist/src/debug/effects.js +228 -0
  55. package/dist/src/debug/effects.js.map +1 -0
  56. package/dist/src/debug/effects.test.d.ts +2 -0
  57. package/dist/src/debug/effects.test.d.ts.map +1 -0
  58. package/dist/src/debug/effects.test.js +84 -0
  59. package/dist/src/debug/effects.test.js.map +1 -0
  60. package/dist/src/debug/files.d.ts +14 -0
  61. package/dist/src/debug/files.d.ts.map +1 -0
  62. package/dist/src/debug/files.js +40 -0
  63. package/dist/src/debug/files.js.map +1 -0
  64. package/dist/src/debug/files.test.d.ts +2 -0
  65. package/dist/src/debug/files.test.d.ts.map +1 -0
  66. package/dist/src/debug/files.test.js +89 -0
  67. package/dist/src/debug/files.test.js.map +1 -0
  68. package/dist/src/debug/index.d.ts +61 -0
  69. package/dist/src/debug/index.d.ts.map +1 -0
  70. package/dist/src/debug/index.js +69 -0
  71. package/dist/src/debug/index.js.map +1 -0
  72. package/dist/src/debug/render.d.ts +57 -0
  73. package/dist/src/debug/render.d.ts.map +1 -0
  74. package/dist/src/debug/render.js +519 -0
  75. package/dist/src/debug/render.js.map +1 -0
  76. package/dist/src/debug/render.test.d.ts +2 -0
  77. package/dist/src/debug/render.test.d.ts.map +1 -0
  78. package/dist/src/debug/render.test.js +328 -0
  79. package/dist/src/debug/render.test.js.map +1 -0
  80. package/dist/src/debug/serialize.d.ts +9 -0
  81. package/dist/src/debug/serialize.d.ts.map +1 -0
  82. package/dist/src/debug/serialize.js +70 -0
  83. package/dist/src/debug/serialize.js.map +1 -0
  84. package/dist/src/debug/symbols.d.ts +15 -0
  85. package/dist/src/debug/symbols.d.ts.map +1 -0
  86. package/dist/src/debug/symbols.js +173 -0
  87. package/dist/src/debug/symbols.js.map +1 -0
  88. package/dist/src/debug/symbols.test.d.ts +2 -0
  89. package/dist/src/debug/symbols.test.d.ts.map +1 -0
  90. package/dist/src/debug/symbols.test.js +104 -0
  91. package/dist/src/debug/symbols.test.js.map +1 -0
  92. package/dist/src/debug/trace.d.ts +342 -0
  93. package/dist/src/debug/trace.d.ts.map +1 -0
  94. package/dist/src/debug/trace.js +443 -0
  95. package/dist/src/debug/trace.js.map +1 -0
  96. package/dist/src/devtools/devtools-protocol.d.ts +232 -0
  97. package/dist/src/devtools/devtools-protocol.d.ts.map +1 -0
  98. package/dist/src/devtools/devtools-protocol.js +2 -0
  99. package/dist/src/devtools/devtools-protocol.js.map +1 -0
  100. package/dist/src/devtools/devtools-server.browser.d.ts +28 -0
  101. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -0
  102. package/dist/src/devtools/devtools-server.browser.js +36 -0
  103. package/dist/src/devtools/devtools-server.browser.js.map +1 -0
  104. package/dist/src/devtools/devtools-server.d.ts +72 -0
  105. package/dist/src/devtools/devtools-server.d.ts.map +1 -0
  106. package/dist/src/devtools/devtools-server.js +256 -0
  107. package/dist/src/devtools/devtools-server.js.map +1 -0
  108. package/dist/src/devtools/devtools-transport.d.ts +23 -0
  109. package/dist/src/devtools/devtools-transport.d.ts.map +1 -0
  110. package/dist/src/devtools/devtools-transport.js +114 -0
  111. package/dist/src/devtools/devtools-transport.js.map +1 -0
  112. package/dist/src/devtools-entry.browser.d.ts +4 -0
  113. package/dist/src/devtools-entry.browser.d.ts.map +1 -0
  114. package/dist/src/devtools-entry.browser.js +2 -0
  115. package/dist/src/devtools-entry.browser.js.map +1 -0
  116. package/dist/src/devtools-entry.d.ts +4 -0
  117. package/dist/src/devtools-entry.d.ts.map +1 -0
  118. package/dist/src/devtools-entry.js +2 -0
  119. package/dist/src/devtools-entry.js.map +1 -0
  120. package/dist/src/diagnostics.d.ts +34 -0
  121. package/dist/src/diagnostics.d.ts.map +1 -0
  122. package/dist/src/diagnostics.js +89 -0
  123. package/dist/src/diagnostics.js.map +1 -0
  124. package/dist/src/index.d.ts +3 -2
  125. package/dist/src/index.d.ts.map +1 -1
  126. package/dist/src/index.js +3 -2
  127. package/dist/src/index.js.map +1 -1
  128. package/dist/src/print-hook.d.ts +14 -0
  129. package/dist/src/print-hook.d.ts.map +1 -0
  130. package/dist/src/print-hook.js +10 -0
  131. package/dist/src/print-hook.js.map +1 -0
  132. package/dist/src/reactive-union-set.d.ts.map +1 -1
  133. package/dist/src/reactive-union-set.js +28 -3
  134. package/dist/src/reactive-union-set.js.map +1 -1
  135. package/dist/src/reactivity.d.ts +50 -8
  136. package/dist/src/reactivity.d.ts.map +1 -1
  137. package/dist/src/reactivity.js +225 -39
  138. package/dist/src/reactivity.js.map +1 -1
  139. package/dist/src/render-stack.d.ts +18 -1
  140. package/dist/src/render-stack.d.ts.map +1 -1
  141. package/dist/src/render-stack.js +61 -1
  142. package/dist/src/render-stack.js.map +1 -1
  143. package/dist/src/render.d.ts +8 -15
  144. package/dist/src/render.d.ts.map +1 -1
  145. package/dist/src/render.js +370 -109
  146. package/dist/src/render.js.map +1 -1
  147. package/dist/src/resource.d.ts.map +1 -1
  148. package/dist/src/resource.js +5 -0
  149. package/dist/src/resource.js.map +1 -1
  150. package/dist/src/scheduler.d.ts +8 -0
  151. package/dist/src/scheduler.d.ts.map +1 -1
  152. package/dist/src/scheduler.js +69 -3
  153. package/dist/src/scheduler.js.map +1 -1
  154. package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
  155. package/dist/src/symbols/basic-symbol.js +6 -1
  156. package/dist/src/symbols/basic-symbol.js.map +1 -1
  157. package/dist/src/symbols/decl.d.ts.map +1 -1
  158. package/dist/src/symbols/decl.js +5 -1
  159. package/dist/src/symbols/decl.js.map +1 -1
  160. package/dist/src/symbols/output-scope.d.ts +2 -1
  161. package/dist/src/symbols/output-scope.d.ts.map +1 -1
  162. package/dist/src/symbols/output-scope.js +13 -8
  163. package/dist/src/symbols/output-scope.js.map +1 -1
  164. package/dist/src/symbols/output-symbol.d.ts +1 -0
  165. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  166. package/dist/src/symbols/output-symbol.js +25 -8
  167. package/dist/src/symbols/output-symbol.js.map +1 -1
  168. package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
  169. package/dist/src/symbols/symbol-flow.js +24 -8
  170. package/dist/src/symbols/symbol-flow.js.map +1 -1
  171. package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
  172. package/dist/src/symbols/symbol-slot.js +15 -0
  173. package/dist/src/symbols/symbol-slot.js.map +1 -1
  174. package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
  175. package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
  176. package/dist/src/symbols/symbol-slot.test.js +35 -0
  177. package/dist/src/symbols/symbol-slot.test.js.map +1 -0
  178. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  179. package/dist/src/symbols/symbol-table.js +6 -5
  180. package/dist/src/symbols/symbol-table.js.map +1 -1
  181. package/dist/src/trace.d.ts +2 -0
  182. package/dist/src/trace.d.ts.map +1 -0
  183. package/dist/src/trace.js +2 -0
  184. package/dist/src/trace.js.map +1 -0
  185. package/dist/src/tracer.d.ts +2 -228
  186. package/dist/src/tracer.d.ts.map +1 -1
  187. package/dist/src/tracer.js +5 -298
  188. package/dist/src/tracer.js.map +1 -1
  189. package/dist/src/utils.d.ts.map +1 -1
  190. package/dist/src/utils.js +7 -5
  191. package/dist/src/utils.js.map +1 -1
  192. package/dist/test/components/append-file.test.d.ts.map +1 -1
  193. package/dist/test/components/append-file.test.js +18 -10
  194. package/dist/test/components/append-file.test.js.map +1 -1
  195. package/dist/test/components/template-file.test.d.ts.map +1 -1
  196. package/dist/test/components/template-file.test.js +6 -4
  197. package/dist/test/components/template-file.test.js.map +1 -1
  198. package/dist/test/lazy-isempty.test.d.ts +2 -0
  199. package/dist/test/lazy-isempty.test.d.ts.map +1 -0
  200. package/dist/test/lazy-isempty.test.js +89 -0
  201. package/dist/test/lazy-isempty.test.js.map +1 -0
  202. package/dist/test/reactive-union-set-disposers.test.d.ts +2 -0
  203. package/dist/test/reactive-union-set-disposers.test.d.ts.map +1 -0
  204. package/dist/test/reactive-union-set-disposers.test.js +98 -0
  205. package/dist/test/reactive-union-set-disposers.test.js.map +1 -0
  206. package/dist/test/reactivity/shallow-reactive.test.d.ts +2 -0
  207. package/dist/test/reactivity/shallow-reactive.test.d.ts.map +1 -0
  208. package/dist/test/reactivity/shallow-reactive.test.js +52 -0
  209. package/dist/test/reactivity/shallow-reactive.test.js.map +1 -0
  210. package/dist/test/rendering/basic.test.js +3 -0
  211. package/dist/test/rendering/basic.test.js.map +1 -1
  212. package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -1
  213. package/dist/test/rendering/print-render-stack.test.js +91 -98
  214. package/dist/test/rendering/print-render-stack.test.js.map +1 -1
  215. package/dist/test/scheduler-extended.test.d.ts +2 -0
  216. package/dist/test/scheduler-extended.test.d.ts.map +1 -0
  217. package/dist/test/scheduler-extended.test.js +96 -0
  218. package/dist/test/scheduler-extended.test.js.map +1 -0
  219. package/dist/test/scheduler.test.d.ts +2 -0
  220. package/dist/test/scheduler.test.d.ts.map +1 -0
  221. package/dist/test/scheduler.test.js +46 -0
  222. package/dist/test/scheduler.test.js.map +1 -0
  223. package/dist/testing/create-test-wrapper.d.ts +1 -1
  224. package/dist/testing/create-test-wrapper.d.ts.map +1 -1
  225. package/dist/testing/create-test-wrapper.js +1 -1
  226. package/dist/testing/create-test-wrapper.js.map +1 -1
  227. package/dist/testing/devtools-utils.d.ts +26 -0
  228. package/dist/testing/devtools-utils.d.ts.map +1 -0
  229. package/dist/testing/devtools-utils.js +140 -0
  230. package/dist/testing/devtools-utils.js.map +1 -0
  231. package/dist/testing/extend-expect.d.ts.map +1 -1
  232. package/dist/testing/extend-expect.js +63 -1
  233. package/dist/testing/extend-expect.js.map +1 -1
  234. package/dist/testing/render.d.ts +2 -2
  235. package/dist/testing/render.d.ts.map +1 -1
  236. package/dist/testing/render.js +2 -2
  237. package/dist/testing/render.js.map +1 -1
  238. package/dist/tsconfig.tsbuildinfo +1 -1
  239. package/package.json +21 -7
  240. package/scripts/copy-devtools-ui.mjs +26 -0
  241. package/src/binder.ts +71 -16
  242. package/src/components/AccessExpression.test.tsx +132 -0
  243. package/src/components/AccessExpression.tsx +344 -0
  244. package/src/components/AppendFile.tsx +14 -9
  245. package/src/components/Block.tsx +1 -1
  246. package/src/components/Declaration.tsx +2 -1
  247. package/src/components/Prose.tsx +1 -1
  248. package/src/components/Scope.tsx +6 -1
  249. package/src/components/SourceDirectory.tsx +1 -2
  250. package/src/components/TemplateFile.tsx +18 -9
  251. package/src/components/index.tsx +1 -0
  252. package/src/content-slot.tsx +7 -7
  253. package/src/context.ts +17 -6
  254. package/src/{debug.ts → debug/cli.ts} +114 -125
  255. package/src/debug/diagnostics.test.tsx +55 -0
  256. package/src/debug/effects.test.tsx +89 -0
  257. package/src/debug/effects.ts +317 -0
  258. package/src/debug/files.test.tsx +96 -0
  259. package/src/debug/files.ts +40 -0
  260. package/src/debug/index.ts +128 -0
  261. package/src/debug/render.test.tsx +379 -0
  262. package/src/debug/render.ts +639 -0
  263. package/src/debug/serialize.ts +85 -0
  264. package/src/debug/symbols.test.tsx +106 -0
  265. package/src/debug/symbols.ts +239 -0
  266. package/src/debug/trace.ts +312 -0
  267. package/src/devtools/devtools-protocol.ts +312 -0
  268. package/src/devtools/devtools-server.browser.ts +71 -0
  269. package/src/devtools/devtools-server.ts +290 -0
  270. package/src/devtools/devtools-transport.ts +154 -0
  271. package/src/devtools-entry.browser.ts +52 -0
  272. package/src/devtools-entry.ts +54 -0
  273. package/src/diagnostics.ts +141 -0
  274. package/src/index.ts +2 -7
  275. package/src/print-hook.ts +22 -0
  276. package/src/reactive-union-set.ts +85 -44
  277. package/src/reactivity.ts +301 -59
  278. package/src/render-stack.ts +73 -1
  279. package/src/render.ts +470 -161
  280. package/src/resource.ts +28 -19
  281. package/src/scheduler.ts +80 -4
  282. package/src/symbols/basic-symbol.ts +6 -1
  283. package/src/symbols/decl.ts +5 -1
  284. package/src/symbols/output-scope.ts +21 -13
  285. package/src/symbols/output-symbol.ts +34 -14
  286. package/src/symbols/symbol-flow.ts +76 -39
  287. package/src/symbols/symbol-slot.test.tsx +41 -0
  288. package/src/symbols/symbol-slot.tsx +47 -20
  289. package/src/symbols/symbol-table.ts +6 -10
  290. package/src/trace.ts +1 -0
  291. package/src/tracer.ts +13 -242
  292. package/src/utils.tsx +24 -17
  293. package/temp/api.json +5658 -3095
  294. package/test/components/append-file.test.tsx +36 -29
  295. package/test/components/template-file.test.tsx +11 -11
  296. package/test/lazy-isempty.test.tsx +106 -0
  297. package/test/reactive-union-set-disposers.test.tsx +112 -0
  298. package/test/reactivity/shallow-reactive.test.tsx +56 -0
  299. package/test/rendering/basic.test.tsx +4 -0
  300. package/test/rendering/print-render-stack.test.tsx +52 -43
  301. package/test/scheduler-extended.test.tsx +122 -0
  302. package/test/scheduler.test.tsx +50 -0
  303. package/testing/create-test-wrapper.tsx +1 -1
  304. package/testing/devtools-utils.ts +203 -0
  305. package/testing/extend-expect.ts +89 -0
  306. package/testing/render.ts +2 -2
  307. package/testing/vitest.d.ts +9 -0
  308. package/dist/src/debug.d.ts +0 -14
  309. package/dist/src/debug.d.ts.map +0 -1
  310. package/dist/src/debug.js.map +0 -1
package/src/reactivity.ts CHANGED
@@ -2,21 +2,28 @@ import {
2
2
  isRef,
3
3
  pauseTracking,
4
4
  ReactiveEffectRunner,
5
- ref,
6
5
  Ref,
7
6
  resetTracking,
8
7
  ShallowReactive,
9
- shallowRef,
10
8
  stop,
9
+ computed as vueComputed,
11
10
  effect as vueEffect,
11
+ ref as vueRef,
12
+ shallowReactive as vueShallowReactive,
13
+ shallowRef as vueShallowRef,
14
+ toRef as vueToRef,
15
+ toRefs as vueToRefs,
12
16
  } from "@vue/reactivity";
13
- import type { RenderedTextTree } from "./render.js";
14
- import type { Children, ComponentCreator } from "./runtime/component.js";
17
+ import {
18
+ captureSourceLocation,
19
+ debug,
20
+ isDevtoolsEnabled,
21
+ } from "./debug/index.js";
22
+ import { RenderedTextTree } from "./render.js";
23
+ import { Children, ComponentCreator } from "./runtime/component.js";
15
24
  import { scheduler } from "./scheduler.js";
16
25
  import type { OutputSymbol } from "./symbols/output-symbol.js";
17
- import { trace, TracePhase } from "./tracer.js";
18
26
 
19
- // check for multiple versions of alloy here.
20
27
  if ((globalThis as any).__ALLOY__) {
21
28
  throw new Error(
22
29
  "Multiple versions of Alloy are loaded for this project. This will likely cause undesirable behavior.",
@@ -25,7 +32,8 @@ if ((globalThis as any).__ALLOY__) {
25
32
  (globalThis as any).__ALLOY__ = true;
26
33
 
27
34
  export function getElementCache() {
28
- return getContext()!.elementCache;
35
+ const ctx = getContext()!;
36
+ return (ctx.elementCache ??= new Map());
29
37
  }
30
38
 
31
39
  export type ElementCacheKey =
@@ -39,8 +47,12 @@ export interface Disposable {
39
47
  (): void;
40
48
  }
41
49
 
50
+ let contextIdCounter = 0;
51
+
42
52
  export interface Context {
43
- disposables: Disposable[];
53
+ /** Monotonic numeric ID for trace/debug correlation. */
54
+ id: number;
55
+ disposables?: Disposable[];
44
56
  owner: Context | null;
45
57
 
46
58
  // context providers
@@ -53,7 +65,7 @@ export interface Context {
53
65
  * A cache of RenderTextTree nodes created within this context,
54
66
  * indexed by the component or function which created them.
55
67
  */
56
- elementCache: ElementCache;
68
+ elementCache?: ElementCache;
57
69
  /**
58
70
  * When this context was created by a component, this will
59
71
  * be the component that created it.
@@ -78,9 +90,17 @@ export interface Context {
78
90
 
79
91
  /**
80
92
  * A ref that indicates whether the component is empty.
93
+ * Only allocated when reactively observed (ContentSlot, mapJoin).
81
94
  */
82
95
  isEmpty?: Ref<boolean>;
83
96
 
97
+ /**
98
+ * Cheap boolean tracking the last propagated empty state.
99
+ * Used by notifyContentState() for early-return optimization
100
+ * without requiring a reactive ref on every context.
101
+ */
102
+ _lastEmpty: boolean;
103
+
84
104
  /**
85
105
  * Whether this context is a root context
86
106
  */
@@ -92,21 +112,46 @@ export function getContext() {
92
112
  return globalContext;
93
113
  }
94
114
 
115
+ /**
116
+ * Walk up the owner chain to find the nearest ancestor context that
117
+ * corresponds to an effect (has meta.effectId). This bridges non-effect
118
+ * scopes (like createRoot iterations in For) so the owner chain always
119
+ * connects effect-to-effect.
120
+ */
121
+ function resolveOwnerEffectContextId(context: Context): number | null {
122
+ let owner = context.owner;
123
+ while (owner) {
124
+ if (owner.meta?.effectId !== undefined) {
125
+ return owner.id;
126
+ }
127
+ owner = owner.owner;
128
+ }
129
+ return context.owner?.id ?? null;
130
+ }
131
+
132
+ /**
133
+ * Ensure that a context has an isEmpty ref, creating one if needed.
134
+ * Only call this when you need to reactively observe isEmpty (e.g.,
135
+ * ContentSlot, mapJoin). Most contexts don't need an isEmpty ref.
136
+ */
137
+ export function ensureIsEmpty(context: Context): Ref<boolean> {
138
+ context.isEmpty ??= ref(context.childrenWithContent === 0);
139
+ return context.isEmpty;
140
+ }
141
+
95
142
  export interface RootOptions {
96
143
  componentOwner?: ComponentCreator<any>;
97
144
  }
98
145
 
99
146
  export function root<T>(fn: (d: Disposable) => T, options?: RootOptions): T {
100
147
  const context: Context = {
148
+ id: contextIdCounter++,
101
149
  componentOwner: options?.componentOwner,
102
- disposables: [],
103
150
  owner: globalContext,
104
- context: {},
105
- elementCache: new Map(),
106
151
  takesSymbols: false,
107
152
  takenSymbols: undefined,
108
153
  childrenWithContent: 0,
109
- isEmpty: ref(true),
154
+ _lastEmpty: true,
110
155
  isRoot: true,
111
156
  };
112
157
 
@@ -115,8 +160,8 @@ export function root<T>(fn: (d: Disposable) => T, options?: RootOptions): T {
115
160
  try {
116
161
  ret = untrack(() =>
117
162
  fn(() => {
118
- for (const d of context!.disposables) {
119
- d();
163
+ for (const d of context!.disposables ?? []) {
164
+ untrack(d);
120
165
  }
121
166
  }),
122
167
  );
@@ -127,6 +172,15 @@ export function root<T>(fn: (d: Disposable) => T, options?: RootOptions): T {
127
172
  return ret;
128
173
  }
129
174
 
175
+ export interface EffectDebugOptions {
176
+ name?: string;
177
+ type?: string;
178
+ }
179
+
180
+ export interface EffectOptions {
181
+ debug?: EffectDebugOptions;
182
+ }
183
+
130
184
  export function untrack<T>(fn: () => T): T {
131
185
  pauseTracking();
132
186
  const v = fn();
@@ -134,68 +188,148 @@ export function untrack<T>(fn: () => T): T {
134
188
  return v;
135
189
  }
136
190
 
137
- export function memo<T>(fn: () => T, equal?: boolean): () => T {
138
- const o = shallowRef();
139
- effect((prev) => {
140
- const res = fn();
141
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
142
- (!equal || prev !== res) && (o.value = res);
143
- return res;
144
- }, undefined as T);
145
- return () => o.value;
191
+ /**
192
+ * Walk up the context owner chain to find the nearest effect ID.
193
+ * Used to attribute reactive mutations to the effect that caused them.
194
+ */
195
+ export function findCurrentEffectId(): number | undefined {
196
+ let ctx = globalContext;
197
+ while (ctx) {
198
+ const id = ctx.meta?.effectId;
199
+ if (id !== undefined && id !== -1) return id;
200
+ ctx = ctx.owner;
201
+ }
202
+ return undefined;
146
203
  }
147
204
 
148
- export function effect<T>(fn: (prev?: T) => T, current?: T) {
205
+ export function memo<T>(fn: () => T, equal?: boolean, name?: string): () => T {
206
+ const o = shallowRef<T>();
207
+ effect(
208
+ (prev) => {
209
+ const res = fn();
210
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
211
+ (!equal || prev !== res) && (o.value = res);
212
+ return res;
213
+ },
214
+ undefined as T,
215
+ {
216
+ debug: { name: name ? `memo:${name}` : "memo" },
217
+ },
218
+ );
219
+ const getter = (() => o.value as T) as () => T;
220
+ if (name) {
221
+ Object.defineProperty(getter, "name", { value: name, configurable: true });
222
+ }
223
+ return getter;
224
+ }
225
+
226
+ export function effect<T>(
227
+ fn: (prev?: T) => T,
228
+ current?: T,
229
+ options?: EffectOptions,
230
+ ) {
149
231
  const context: Context = {
150
- context: {},
151
- disposables: [] as (() => void)[],
232
+ id: contextIdCounter++,
152
233
  owner: globalContext,
153
- elementCache: new Map(),
154
234
  takesSymbols: false,
155
235
  takenSymbols: undefined,
156
236
  childrenWithContent: 0,
237
+ _lastEmpty: true,
157
238
  isRoot: false,
158
239
  };
159
240
 
241
+ const debugInfo = options?.debug;
242
+ const effectId = debug.effect.register({
243
+ name: debugInfo?.name ?? fn.name,
244
+ type: debugInfo?.type,
245
+ createdAt: captureSourceLocation(),
246
+ contextId: context.id,
247
+ ownerContextId: resolveOwnerEffectContextId(context),
248
+ });
249
+
250
+ if (effectId !== -1) {
251
+ context.meta ??= {};
252
+ context.meta.effectId = effectId;
253
+ }
254
+
160
255
  const cleanupFn = (final: boolean) => {
161
256
  const d = context.disposables;
162
- context.disposables = [];
163
- for (let k = 0, len = d.length; k < len; k++) d[k]();
257
+ context.disposables = undefined;
258
+ if (d) {
259
+ for (let k = 0, len = d.length; k < len; k++) untrack(d[k]);
260
+ }
164
261
 
165
262
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
166
263
  final && stop(runner);
167
264
  };
168
265
 
169
266
  onCleanup(() => cleanupFn(true));
170
- const runner: ReactiveEffectRunner<void> = vueEffect(
171
- () => {
172
- cleanupFn(false);
173
-
174
- const oldContext = globalContext;
175
- globalContext = context;
176
- try {
177
- current = fn(current);
178
- } finally {
179
- globalContext = oldContext;
267
+ const effectOpts: Record<string, unknown> = {
268
+ // allow recursive effects with 32, 1 and 4 are default flags
269
+ flags: 1 | 4 | 32,
270
+ scheduler: scheduler(),
271
+ };
272
+
273
+ if (effectId !== -1) {
274
+ effectOpts.onTrack = (event: any) => {
275
+ const targetKey =
276
+ typeof event.key === "symbol" ? event.key.toString() : event.key;
277
+ if (isRef(event.target)) {
278
+ const id = refId(event.target);
279
+ debug.effect.ensureRef({ id, kind: "ref" });
280
+ debug.effect.track({
281
+ effectId,
282
+ target: event.target,
283
+ refId: id,
284
+ targetKey,
285
+ });
286
+ } else {
287
+ debug.effect.track({
288
+ effectId,
289
+ target: event.target,
290
+ targetKey,
291
+ });
180
292
  }
181
- },
182
- {
183
- // allow recursive effects with 32, 1 and 4 are default flags
184
- // @ts-expect-error flags is a vue internal thing
185
- flags: 1 | 4 | 32,
186
- scheduler: scheduler(),
187
- onTrack(event) {
188
- trace(TracePhase.effect.track, () => {
189
- return `tracking ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
293
+ };
294
+ effectOpts.onTrigger = (event: any) => {
295
+ const targetKey =
296
+ typeof event.key === "symbol" ? event.key.toString() : event.key;
297
+ if (isRef(event.target)) {
298
+ const id = refId(event.target);
299
+ debug.effect.ensureRef({ id, kind: "ref" });
300
+ debug.effect.trigger({
301
+ effectId,
302
+ target: event.target,
303
+ refId: id,
304
+ targetKey,
305
+ kind: "triggered-by",
190
306
  });
191
- },
192
- onTrigger(event) {
193
- trace(TracePhase.effect.trigger, () => {
194
- return `triggering ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
307
+ } else {
308
+ debug.effect.trigger({
309
+ effectId,
310
+ target: event.target,
311
+ targetKey,
312
+ kind: "triggered-by",
195
313
  });
196
- },
197
- },
198
- );
314
+ }
315
+ };
316
+ }
317
+
318
+ const runner: ReactiveEffectRunner<void> = vueEffect(() => {
319
+ cleanupFn(false);
320
+
321
+ const oldContext = globalContext;
322
+ globalContext = context;
323
+ try {
324
+ current = fn(current);
325
+ } finally {
326
+ globalContext = oldContext;
327
+ }
328
+ }, effectOpts as any);
329
+
330
+ if (effectId !== -1) {
331
+ effectIdMap.set(runner.effect, effectId);
332
+ }
199
333
  }
200
334
 
201
335
  /**
@@ -218,7 +352,7 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
218
352
  */
219
353
  export function onCleanup(fn: Disposable) {
220
354
  if (globalContext != null) {
221
- globalContext.disposables.push(fn);
355
+ (globalContext.disposables ??= []).push(fn);
222
356
  }
223
357
  }
224
358
 
@@ -253,14 +387,122 @@ export function isCustomContext(child: Children): child is CustomContext {
253
387
  );
254
388
  }
255
389
 
390
+ export function ref<T>(
391
+ value?: T,
392
+ options?: { isInfrastructure?: boolean },
393
+ ): Ref<T> {
394
+ const result = vueRef(value) as Ref<T>;
395
+ debug.effect.registerRef({
396
+ id: refId(result, options?.isInfrastructure),
397
+ kind: "ref",
398
+ createdAt: captureSourceLocation(),
399
+ createdByEffectId: globalContext?.meta?.effectId,
400
+ isInfrastructure: options?.isInfrastructure,
401
+ });
402
+ return result;
403
+ }
404
+
405
+ // Stores creation location for shallowReactive objects so registerNonRefTarget
406
+ // can look it up later (since targets are lazily registered on first track/trigger).
407
+ const reactiveCreationLocations = new WeakMap<
408
+ object,
409
+ ReturnType<typeof captureSourceLocation>
410
+ >();
411
+
412
+ export function getReactiveCreationLocation(target: object) {
413
+ return reactiveCreationLocations.get(target);
414
+ }
415
+
416
+ export function shallowReactive<T extends object>(
417
+ target: T,
418
+ ): ShallowReactive<T> {
419
+ const result = vueShallowReactive(target);
420
+ if (isDevtoolsEnabled()) {
421
+ // Store by raw target — Vue's onTrack/onTrigger events pass the raw object, not the proxy.
422
+ reactiveCreationLocations.set(target, captureSourceLocation());
423
+ }
424
+ return result;
425
+ }
426
+
427
+ export function shallowRef<T>(value?: T): Ref<T> {
428
+ const result = vueShallowRef(value) as Ref<T>;
429
+ debug.effect.registerRef({
430
+ id: refId(result),
431
+ kind: "shallowRef",
432
+ createdAt: captureSourceLocation(),
433
+ createdByEffectId: globalContext?.meta?.effectId,
434
+ });
435
+ return result;
436
+ }
437
+
438
+ export function computed<T>(getter: () => T): Ref<T> {
439
+ const result = vueComputed(getter) as Ref<T>;
440
+ debug.effect.registerRef({
441
+ id: refId(result),
442
+ kind: "computed",
443
+ createdAt: captureSourceLocation(),
444
+ createdByEffectId: globalContext?.meta?.effectId,
445
+ });
446
+ return result;
447
+ }
448
+
449
+ export function toRef<T extends object, K extends keyof T>(
450
+ object: T,
451
+ key: K,
452
+ defaultValue?: T[K],
453
+ ): Ref<T[K]> {
454
+ const result =
455
+ defaultValue === undefined ?
456
+ (vueToRef(object, key) as Ref<T[K]>)
457
+ : (vueToRef(object, key, defaultValue) as Ref<T[K]>);
458
+ debug.effect.registerRef({
459
+ id: refId(result),
460
+ kind: "toRef",
461
+ createdAt: captureSourceLocation(),
462
+ createdByEffectId: globalContext?.meta?.effectId,
463
+ });
464
+ return result;
465
+ }
466
+
467
+ export function toRefs<T extends object>(
468
+ object: T,
469
+ ): { [K in keyof T]: Ref<T[K]> } {
470
+ const result = vueToRefs(object) as { [K in keyof T]: Ref<T[K]> };
471
+ for (const refValue of Object.values(result) as Ref<unknown>[]) {
472
+ debug.effect.registerRef({
473
+ id: refId(refValue),
474
+ kind: "toRef",
475
+ createdAt: captureSourceLocation(),
476
+ createdByEffectId: globalContext?.meta?.effectId,
477
+ });
478
+ }
479
+ return result;
480
+ }
481
+
256
482
  const seenRefs = new WeakMap<Ref<unknown>, number>();
257
483
  let refIdCounter = 1;
484
+ let infraRefIdCounter = -1;
485
+ const effectIdMap = new WeakMap<object, number>();
258
486
 
259
- export function refId(ref: Ref<unknown>): number {
487
+ export function refId(ref: Ref<unknown>, isInfrastructure?: boolean): number {
260
488
  let id = seenRefs.get(ref);
261
489
  if (id === undefined) {
262
- id = refIdCounter++;
490
+ id = isInfrastructure ? infraRefIdCounter-- : refIdCounter++;
263
491
  seenRefs.set(ref, id);
264
492
  }
265
493
  return id;
266
494
  }
495
+
496
+ /** Allocate a unique reactive target ID from the same counter space as ref IDs. */
497
+ export function nextReactiveId(): number {
498
+ return refIdCounter++;
499
+ }
500
+
501
+ export function resetRefIdCounter(): void {
502
+ refIdCounter = 1;
503
+ infraRefIdCounter = -1;
504
+ }
505
+
506
+ export function getEffectDebugId(effect: object): number | undefined {
507
+ return effectIdMap.get(effect);
508
+ }
@@ -13,6 +13,14 @@ const renderStack: Array<{
13
13
  source?: SourceLocation;
14
14
  }> = [];
15
15
 
16
+ export interface RenderStackSnapshotEntry {
17
+ component: Component<any>;
18
+ props: Props;
19
+ context: Context | null;
20
+ source?: SourceLocation;
21
+ displayName: string;
22
+ }
23
+
16
24
  export function pushStack(
17
25
  component: Component<any>,
18
26
  props: Props,
@@ -25,10 +33,51 @@ export function popStack() {
25
33
  renderStack.pop();
26
34
  }
27
35
 
36
+ export function currentComponentName(): string | undefined {
37
+ const entry = renderStack[renderStack.length - 1];
38
+ return entry?.component.name || undefined;
39
+ }
40
+
28
41
  export function clearRenderStack() {
29
42
  renderStack.length = 0;
30
43
  }
31
44
 
45
+ /**
46
+ * Get the current rendering path from the render stack context.
47
+ * Prefers SourceFileContext over SourceDirectoryContext.
48
+ */
49
+ export function getCurrentRenderPath(): string | undefined {
50
+ let currentPath: string | undefined;
51
+ for (let i = renderStack.length - 1; i >= 0; i--) {
52
+ const { context } = renderStack[i];
53
+ // Prefer SourceFileContext over SourceDirectoryContext
54
+ if (context?.context?.[SourceFileContext.id]) {
55
+ const fileContext = context.context[
56
+ SourceFileContext.id
57
+ ] as SourceFileContext;
58
+ return fileContext.path;
59
+ }
60
+ if (!currentPath && context?.context?.[SourceDirectoryContext.id]) {
61
+ const dirContext = context.context[
62
+ SourceDirectoryContext.id
63
+ ] as SourceDirectoryContext;
64
+ currentPath = dirContext.path;
65
+ // Don't break - keep looking for a SourceFileContext
66
+ }
67
+ }
68
+ return currentPath;
69
+ }
70
+
71
+ export function getRenderStackSnapshot(): RenderStackSnapshotEntry[] {
72
+ return renderStack.map((entry) => ({
73
+ component: entry.component,
74
+ props: entry.props,
75
+ context: entry.context,
76
+ source: entry.source,
77
+ displayName: getComponentDisplayName(entry.component, entry.props),
78
+ }));
79
+ }
80
+
32
81
  // Helper functions
33
82
  function getComponentDisplayName(
34
83
  component: Component<any>,
@@ -93,8 +142,10 @@ function formatSourceLocation(source: SourceLocation): string {
93
142
  * This differs from debug.component.stack in that this uses a purpose-built
94
143
  * stack rather than walking the context chain. When this is called, the context
95
144
  * chain has been restored. In the future this can probably be unified nicely.
145
+ *
146
+ * @param error - Optional error to print the stack trace from
96
147
  */
97
- export function printRenderStack() {
148
+ export function printRenderStack(error?: unknown) {
98
149
  // Find the nearest SourceFileContext or SourceDirectoryContext from the render stack
99
150
  let currentPath: string | undefined;
100
151
  for (let i = renderStack.length - 1; i >= 0; i--) {
@@ -124,6 +175,27 @@ export function printRenderStack() {
124
175
  console.error(pc.red("Error rendering:"));
125
176
  }
126
177
 
178
+ // Print the error message and stack if provided
179
+ if (error) {
180
+ if (error instanceof Error) {
181
+ // eslint-disable-next-line no-console
182
+ console.error(pc.red(` ${error.message}`));
183
+ if (error.stack) {
184
+ // Print stack lines (skip the first line which is the error message)
185
+ const stackLines = error.stack.split("\n").slice(1);
186
+ for (const line of stackLines) {
187
+ // eslint-disable-next-line no-console
188
+ console.error(pc.gray(line));
189
+ }
190
+ }
191
+ } else {
192
+ // eslint-disable-next-line no-console
193
+ console.error(pc.red(` ${String(error)}`));
194
+ }
195
+ // eslint-disable-next-line no-console
196
+ console.error(); // Empty line before component stack
197
+ }
198
+
127
199
  // First pass: determine which providers should be nested vs standalone
128
200
  // A provider should be nested under its parent if it's from a different file
129
201
  // (i.e., it's part of the component's implementation, not user-provided)