@luna_ui/luna 0.3.3 → 0.3.5

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 (174) hide show
  1. package/dist/cli.mjs +1264 -27
  2. package/dist/css/index.d.ts +194 -0
  3. package/dist/css/index.js +721 -0
  4. package/dist/css/runtime.d.ts +92 -0
  5. package/dist/css/runtime.js +179 -0
  6. package/dist/index.js +1 -1
  7. package/dist/jsx-dev-runtime.js +1 -1
  8. package/dist/jsx-runtime.d.ts +5 -0
  9. package/dist/jsx-runtime.js +1 -1
  10. package/dist/src-CHiGeWfy.js +1 -0
  11. package/dist/vite-plugin.d.ts +122 -0
  12. package/dist/vite-plugin.js +1518 -0
  13. package/package.json +16 -2
  14. package/src/css/extract.ts +798 -0
  15. package/src/css/index.ts +10 -0
  16. package/src/css/inject.ts +205 -0
  17. package/src/css/inline.ts +182 -0
  18. package/src/css/minify.ts +70 -0
  19. package/src/css/optimizer.ts +6 -0
  20. package/src/css/runtime.ts +344 -0
  21. package/src/css-optimizer/README.md +353 -0
  22. package/src/css-optimizer/cooccurrence.ts +100 -0
  23. package/src/css-optimizer/core.ts +263 -0
  24. package/src/css-optimizer/extractors.ts +243 -0
  25. package/src/css-optimizer/hash.ts +54 -0
  26. package/src/css-optimizer/index.ts +129 -0
  27. package/src/css-optimizer/merge.ts +109 -0
  28. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  29. package/src/css-optimizer/parser.ts +120 -0
  30. package/src/css-optimizer/pattern.ts +171 -0
  31. package/src/css-optimizer/transformers.ts +301 -0
  32. package/src/css-optimizer/types.ts +128 -0
  33. package/src/event-utils.ts +227 -0
  34. package/src/index.ts +890 -0
  35. package/src/jsx-dev-runtime.ts +2 -0
  36. package/src/jsx-runtime.ts +398 -0
  37. package/src/vite-plugin.ts +718 -0
  38. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  39. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  40. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  41. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  42. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  43. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  44. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  45. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  46. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  50. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  58. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  59. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
  60. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
  61. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
  62. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
  63. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
  64. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  65. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  66. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  67. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  68. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  69. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  70. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  71. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  72. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  73. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  74. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  75. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
  76. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  77. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  117. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  118. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  119. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  120. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  121. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  122. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  123. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  124. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  125. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  137. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  138. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  139. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  140. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  141. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  142. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  143. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  144. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  145. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  147. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  148. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  149. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  150. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  151. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  152. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  153. package/tests/context.test.ts +118 -0
  154. package/tests/css-optimizer-extractors.test.ts +264 -0
  155. package/tests/css-optimizer-integration.test.ts +566 -0
  156. package/tests/css-optimizer-transformers.test.ts +301 -0
  157. package/tests/css-optimizer.test.ts +646 -0
  158. package/tests/css-runtime.bench.ts +442 -0
  159. package/tests/css-runtime.test.ts +342 -0
  160. package/tests/dom.test.ts +872 -0
  161. package/tests/integration.test.ts +405 -0
  162. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  163. package/tests/jsx-runtime.test.tsx +393 -0
  164. package/tests/lifecycle.test.ts +833 -0
  165. package/tests/move-before.bench.ts +304 -0
  166. package/tests/preact-signals-comparison.test.ts +1608 -0
  167. package/tests/resource.test.ts +160 -0
  168. package/tests/router.test.ts +117 -0
  169. package/tests/show-initial-mount-leak.test.tsx +182 -0
  170. package/tests/solidjs-api.test.ts +659 -0
  171. package/tests/static-perf.bench.ts +64 -0
  172. package/tests/store.test.ts +263 -0
  173. package/tests/tsx-syntax.test.tsx +404 -0
  174. package/dist/src-DGWY0NYx.js +0 -1
@@ -0,0 +1,833 @@
1
+ /**
2
+ * Lifecycle tests for onMount and onCleanup
3
+ * Based on Solid.js semantics
4
+ *
5
+ * Expected behavior:
6
+ * - onMount runs AFTER DOM is created and refs are bound
7
+ * - onCleanup registered inside onMount should run when component unmounts
8
+ * - onCleanup registered inside effects should run when effect re-runs or component unmounts
9
+ */
10
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
11
+ import {
12
+ createSignal,
13
+ createEffect,
14
+ createRenderEffect,
15
+ createRoot,
16
+ onMount,
17
+ onCleanup,
18
+ createElement,
19
+ text,
20
+ textDyn,
21
+ render,
22
+ show,
23
+ Show,
24
+ For,
25
+ } from "../src/index";
26
+
27
+ // MoonBit AttrValue constructors
28
+ const AttrValue = {
29
+ Static: (value: string) => ({ $tag: 0, _0: value }),
30
+ Dynamic: (getter: () => string) => ({ $tag: 1, _0: getter }),
31
+ Handler: (handler: (e: unknown) => void) => ({ $tag: 2, _0: handler }),
32
+ };
33
+
34
+ function attr(name: string, value: unknown) {
35
+ return { _0: name, _1: value };
36
+ }
37
+
38
+ // Helper to wait for microtasks to complete
39
+ const flushMicrotasks = () => new Promise<void>(resolve => queueMicrotask(resolve));
40
+
41
+ describe("onMount Basic Behavior", () => {
42
+ let container: HTMLElement;
43
+
44
+ beforeEach(() => {
45
+ container = document.createElement("div");
46
+ document.body.appendChild(container);
47
+ });
48
+
49
+ afterEach(() => {
50
+ container.remove();
51
+ });
52
+
53
+ test("onMount runs after component creation", async () => {
54
+ const log: string[] = [];
55
+
56
+ function Component() {
57
+ log.push("component function");
58
+ onMount(() => {
59
+ log.push("onMount");
60
+ });
61
+ log.push("before return");
62
+ return createElement("div", [], [text("content")]);
63
+ }
64
+
65
+ createRoot(() => {
66
+ render(container, Component());
67
+ });
68
+
69
+ // Wait for microtasks (onMount deferred execution)
70
+ await flushMicrotasks();
71
+
72
+ // onMount should run after component function completes
73
+ expect(log).toContain("component function");
74
+ expect(log).toContain("before return");
75
+ expect(log).toContain("onMount");
76
+
77
+ // Order: component function -> before return -> onMount
78
+ const componentIdx = log.indexOf("component function");
79
+ const beforeReturnIdx = log.indexOf("before return");
80
+ const onMountIdx = log.indexOf("onMount");
81
+
82
+ expect(componentIdx).toBeLessThan(beforeReturnIdx);
83
+ expect(beforeReturnIdx).toBeLessThan(onMountIdx);
84
+ });
85
+
86
+ test("onMount has access to DOM refs", async () => {
87
+ let refElement: HTMLElement | null = null;
88
+ let mountElement: HTMLElement | null = null;
89
+
90
+ function Component() {
91
+ onMount(() => {
92
+ mountElement = refElement;
93
+ });
94
+ return createElement(
95
+ "div",
96
+ [attr("__ref", AttrValue.Handler((el: any) => { refElement = el; }))],
97
+ [text("content")]
98
+ );
99
+ }
100
+
101
+ createRoot(() => {
102
+ render(container, Component());
103
+ });
104
+
105
+ // Wait for microtasks
106
+ await flushMicrotasks();
107
+
108
+ // By the time onMount runs, ref should be available
109
+ expect(refElement).not.toBeNull();
110
+ expect(mountElement).not.toBeNull();
111
+ expect(mountElement).toBe(refElement);
112
+ });
113
+
114
+ test("ref is bound before onMount runs", async () => {
115
+ const log: string[] = [];
116
+
117
+ function Component() {
118
+ onMount(() => {
119
+ log.push("onMount");
120
+ });
121
+ return createElement(
122
+ "p",
123
+ [attr("__ref", AttrValue.Handler(() => { log.push("ref"); }))],
124
+ [text("content")]
125
+ );
126
+ }
127
+
128
+ createRoot(() => {
129
+ render(container, Component());
130
+ });
131
+
132
+ // Wait for microtasks
133
+ await flushMicrotasks();
134
+
135
+ // ref should be called before onMount
136
+ expect(log).toEqual(["ref", "onMount"]);
137
+ });
138
+
139
+ test("multiple onMount calls run in order", async () => {
140
+ const log: string[] = [];
141
+
142
+ function Component() {
143
+ onMount(() => log.push("first"));
144
+ onMount(() => log.push("second"));
145
+ onMount(() => log.push("third"));
146
+ return createElement("div", [], [text("content")]);
147
+ }
148
+
149
+ createRoot(() => {
150
+ render(container, Component());
151
+ });
152
+
153
+ // Wait for microtasks
154
+ await flushMicrotasks();
155
+
156
+ expect(log).toEqual(["first", "second", "third"]);
157
+ });
158
+ });
159
+
160
+ describe("onCleanup with onMount", () => {
161
+ let container: HTMLElement;
162
+
163
+ beforeEach(() => {
164
+ container = document.createElement("div");
165
+ document.body.appendChild(container);
166
+ });
167
+
168
+ afterEach(() => {
169
+ container.remove();
170
+ });
171
+
172
+ test("onCleanup inside onMount runs on unmount", async () => {
173
+ const log: string[] = [];
174
+ const [visible, setVisible] = createSignal(true);
175
+
176
+ function Child() {
177
+ onMount(() => {
178
+ log.push("mounted");
179
+ onCleanup(() => {
180
+ log.push("cleanup");
181
+ });
182
+ });
183
+ return createElement("p", [], [text("I'm here")]);
184
+ }
185
+
186
+ createRoot(() => {
187
+ render(
188
+ container,
189
+ createElement("div", [], [
190
+ // Use function children so Child is created inside the owner scope
191
+ Show({ when: visible, children: () => Child() }),
192
+ ])
193
+ );
194
+ });
195
+
196
+ // Wait for microtasks
197
+ await flushMicrotasks();
198
+
199
+ expect(log).toEqual(["mounted"]);
200
+ expect(container.querySelector("p")).not.toBeNull();
201
+
202
+ // Unmount the child
203
+ setVisible(false);
204
+
205
+ // Wait for microtasks (in case cleanup is async)
206
+ await flushMicrotasks();
207
+
208
+ expect(log).toEqual(["mounted", "cleanup"]);
209
+ expect(container.querySelector("p")).toBeNull();
210
+ });
211
+
212
+ test("onCleanup works with show() primitive", async () => {
213
+ const log: string[] = [];
214
+ const [visible, setVisible] = createSignal(true);
215
+
216
+ function Child() {
217
+ onMount(() => {
218
+ log.push("mounted");
219
+ onCleanup(() => {
220
+ log.push("cleanup");
221
+ });
222
+ });
223
+ return createElement("span", [], [text("visible")]);
224
+ }
225
+
226
+ createRoot(() => {
227
+ render(
228
+ container,
229
+ createElement("div", [], [
230
+ show(visible, () => Child()),
231
+ ])
232
+ );
233
+ });
234
+
235
+ // Wait for microtasks
236
+ await flushMicrotasks();
237
+
238
+ expect(log).toEqual(["mounted"]);
239
+
240
+ setVisible(false);
241
+ await flushMicrotasks();
242
+ expect(log).toEqual(["mounted", "cleanup"]);
243
+
244
+ // Re-show should mount again
245
+ setVisible(true);
246
+ await flushMicrotasks();
247
+ expect(log).toEqual(["mounted", "cleanup", "mounted"]);
248
+ });
249
+
250
+ test("nested cleanup order (LIFO)", async () => {
251
+ const log: string[] = [];
252
+ const [visible, setVisible] = createSignal(true);
253
+
254
+ function Child() {
255
+ onMount(() => {
256
+ onCleanup(() => log.push("first registered"));
257
+ onCleanup(() => log.push("second registered"));
258
+ onCleanup(() => log.push("third registered"));
259
+ });
260
+ return createElement("div", [], [text("child")]);
261
+ }
262
+
263
+ createRoot(() => {
264
+ render(
265
+ container,
266
+ // Use function children so Child is created inside the owner scope
267
+ Show({ when: visible, children: () => Child() })
268
+ );
269
+ });
270
+
271
+ // Wait for microtasks
272
+ await flushMicrotasks();
273
+
274
+ setVisible(false);
275
+ await flushMicrotasks();
276
+
277
+ // Cleanups run in reverse order (LIFO)
278
+ expect(log).toEqual([
279
+ "third registered",
280
+ "second registered",
281
+ "first registered",
282
+ ]);
283
+ });
284
+ });
285
+
286
+ describe("onCleanup in Effects", () => {
287
+ test("render effect cleanup runs before re-run (synchronous)", () => {
288
+ const log: string[] = [];
289
+ const [count, setCount] = createSignal(0);
290
+
291
+ createRoot(() => {
292
+ // Use createRenderEffect for synchronous testing
293
+ createRenderEffect(() => {
294
+ const current = count();
295
+ log.push(`effect run: ${current}`);
296
+ onCleanup(() => {
297
+ log.push(`cleanup: ${current}`);
298
+ });
299
+ });
300
+ });
301
+
302
+ expect(log).toEqual(["effect run: 0"]);
303
+
304
+ setCount(1);
305
+ expect(log).toEqual(["effect run: 0", "cleanup: 0", "effect run: 1"]);
306
+
307
+ setCount(2);
308
+ expect(log).toEqual([
309
+ "effect run: 0",
310
+ "cleanup: 0",
311
+ "effect run: 1",
312
+ "cleanup: 1",
313
+ "effect run: 2",
314
+ ]);
315
+ });
316
+
317
+ test("deferred effect: initial run is deferred, re-runs are sync", async () => {
318
+ const log: string[] = [];
319
+ const [count, setCount] = createSignal(0);
320
+
321
+ createRoot(() => {
322
+ // createEffect is deferred for initial run
323
+ createEffect(() => {
324
+ const current = count();
325
+ log.push(`effect run: ${current}`);
326
+ onCleanup(() => {
327
+ log.push(`cleanup: ${current}`);
328
+ });
329
+ });
330
+ });
331
+
332
+ // Effect hasn't run yet (initial run is deferred)
333
+ expect(log).toEqual([]);
334
+
335
+ // Wait for microtask - initial run happens
336
+ await flushMicrotasks();
337
+ expect(log).toEqual(["effect run: 0"]);
338
+
339
+ // Subsequent updates trigger synchronous re-runs (after effect is established)
340
+ setCount(1);
341
+ expect(log).toEqual(["effect run: 0", "cleanup: 0", "effect run: 1"]);
342
+
343
+ setCount(2);
344
+ expect(log).toEqual([
345
+ "effect run: 0",
346
+ "cleanup: 0",
347
+ "effect run: 1",
348
+ "cleanup: 1",
349
+ "effect run: 2",
350
+ ]);
351
+ });
352
+ });
353
+
354
+ describe("Timer Cleanup Pattern (from docs)", () => {
355
+ let container: HTMLElement;
356
+
357
+ beforeEach(() => {
358
+ container = document.createElement("div");
359
+ document.body.appendChild(container);
360
+ });
361
+
362
+ afterEach(() => {
363
+ container.remove();
364
+ });
365
+
366
+ test("timer is properly cleaned up on unmount", async () => {
367
+ const [visible, setVisible] = createSignal(true);
368
+ let intervalCleared = false;
369
+
370
+ function Timer() {
371
+ const [count, setCount] = createSignal(0);
372
+
373
+ onMount(() => {
374
+ // This is the pattern from the docs - onCleanup inside onMount
375
+ onCleanup(() => {
376
+ intervalCleared = true;
377
+ });
378
+ });
379
+
380
+ return createElement("p", [], [textDyn(() => `Count: ${count()}`)]);
381
+ }
382
+
383
+ createRoot(() => {
384
+ render(
385
+ container,
386
+ // Use function children so Timer is created inside the owner scope
387
+ Show({ when: visible, children: () => Timer() })
388
+ );
389
+ });
390
+
391
+ await flushMicrotasks();
392
+
393
+ expect(intervalCleared).toBe(false);
394
+
395
+ setVisible(false);
396
+ await flushMicrotasks();
397
+
398
+ expect(intervalCleared).toBe(true);
399
+ });
400
+ });
401
+
402
+ describe("Event Listener Cleanup Pattern (from docs)", () => {
403
+ let container: HTMLElement;
404
+
405
+ beforeEach(() => {
406
+ container = document.createElement("div");
407
+ document.body.appendChild(container);
408
+ });
409
+
410
+ afterEach(() => {
411
+ container.remove();
412
+ });
413
+
414
+ test("event listener is removed on unmount", async () => {
415
+ const [visible, setVisible] = createSignal(true);
416
+ const addedListeners: string[] = [];
417
+ const removedListeners: string[] = [];
418
+
419
+ // Mock document.addEventListener/removeEventListener
420
+ const originalAdd = document.addEventListener;
421
+ const originalRemove = document.removeEventListener;
422
+
423
+ document.addEventListener = (type: string) => {
424
+ addedListeners.push(type);
425
+ };
426
+ document.removeEventListener = (type: string) => {
427
+ removedListeners.push(type);
428
+ };
429
+
430
+ function KeyboardHandler() {
431
+ onMount(() => {
432
+ const handler = (e: KeyboardEvent) => {};
433
+ document.addEventListener("keydown", handler as any);
434
+
435
+ onCleanup(() => {
436
+ document.removeEventListener("keydown", handler as any);
437
+ });
438
+ });
439
+
440
+ return createElement("div", [], [text("Press Escape to close")]);
441
+ }
442
+
443
+ createRoot(() => {
444
+ render(
445
+ container,
446
+ // Use function children so KeyboardHandler is created inside the owner scope
447
+ Show({ when: visible, children: () => KeyboardHandler() })
448
+ );
449
+ });
450
+
451
+ await flushMicrotasks();
452
+
453
+ expect(addedListeners).toEqual(["keydown"]);
454
+ expect(removedListeners).toEqual([]);
455
+
456
+ setVisible(false);
457
+ await flushMicrotasks();
458
+
459
+ expect(removedListeners).toEqual(["keydown"]);
460
+
461
+ // Restore
462
+ document.addEventListener = originalAdd;
463
+ document.removeEventListener = originalRemove;
464
+ });
465
+ });
466
+
467
+ describe("Third-Party Library Pattern (from docs)", () => {
468
+ let container: HTMLElement;
469
+
470
+ beforeEach(() => {
471
+ container = document.createElement("div");
472
+ document.body.appendChild(container);
473
+ });
474
+
475
+ afterEach(() => {
476
+ container.remove();
477
+ });
478
+
479
+ test("library is initialized after DOM ready and cleaned up on unmount", async () => {
480
+ const log: string[] = [];
481
+ const [visible, setVisible] = createSignal(true);
482
+
483
+ // Mock chart library
484
+ class ChartLibrary {
485
+ constructor(container: HTMLElement, options: any) {
486
+ log.push(`init: ${container.tagName}`);
487
+ }
488
+ destroy() {
489
+ log.push("destroy");
490
+ }
491
+ }
492
+
493
+ function Chart() {
494
+ let containerRef: HTMLElement | null = null;
495
+
496
+ onMount(() => {
497
+ if (containerRef) {
498
+ const chart = new ChartLibrary(containerRef, {});
499
+ onCleanup(() => {
500
+ chart.destroy();
501
+ });
502
+ }
503
+ });
504
+
505
+ return createElement(
506
+ "div",
507
+ [
508
+ attr("className", AttrValue.Static("chart-container")),
509
+ attr("__ref", AttrValue.Handler((el: any) => { containerRef = el; })),
510
+ ],
511
+ []
512
+ );
513
+ }
514
+
515
+ createRoot(() => {
516
+ render(
517
+ container,
518
+ // Use function children so Chart is created inside the owner scope
519
+ Show({ when: visible, children: () => Chart() })
520
+ );
521
+ });
522
+
523
+ await flushMicrotasks();
524
+
525
+ // Chart should be initialized with the container element
526
+ expect(log).toEqual(["init: DIV"]);
527
+
528
+ setVisible(false);
529
+ await flushMicrotasks();
530
+
531
+ expect(log).toEqual(["init: DIV", "destroy"]);
532
+ });
533
+ });
534
+
535
+ describe("For loop with cleanup", () => {
536
+ let container: HTMLElement;
537
+
538
+ beforeEach(() => {
539
+ container = document.createElement("div");
540
+ document.body.appendChild(container);
541
+ });
542
+
543
+ afterEach(() => {
544
+ container.remove();
545
+ });
546
+
547
+ test("items are cleaned up when removed from list", async () => {
548
+ const cleanups: string[] = [];
549
+ const [items, setItems] = createSignal(["a", "b", "c"]);
550
+
551
+ function Item({ id }: { id: string }) {
552
+ onMount(() => {
553
+ onCleanup(() => {
554
+ cleanups.push(id);
555
+ });
556
+ });
557
+ return createElement("li", [], [text(id)]);
558
+ }
559
+
560
+ createRoot(() => {
561
+ render(
562
+ container,
563
+ createElement("ul", [], [
564
+ For({
565
+ each: items,
566
+ children: (item: string) => Item({ id: item }),
567
+ }),
568
+ ])
569
+ );
570
+ });
571
+
572
+ await flushMicrotasks();
573
+
574
+ expect(container.querySelectorAll("li").length).toBe(3);
575
+ expect(cleanups).toEqual([]);
576
+
577
+ // Remove one item
578
+ setItems(["a", "c"]);
579
+ await flushMicrotasks();
580
+
581
+ expect(container.querySelectorAll("li").length).toBe(2);
582
+ // "b" should be cleaned up
583
+ expect(cleanups).toContain("b");
584
+ });
585
+ });
586
+
587
+ // ============================================================================
588
+ // Solid.js Style: onCleanup in Component Body (not inside onMount)
589
+ // ============================================================================
590
+ describe("onCleanup in Component Body (Solid.js style)", () => {
591
+ let container: HTMLElement;
592
+
593
+ beforeEach(() => {
594
+ container = document.createElement("div");
595
+ document.body.appendChild(container);
596
+ });
597
+
598
+ afterEach(() => {
599
+ container.remove();
600
+ });
601
+
602
+ test("onCleanup in component body runs on unmount", async () => {
603
+ const log: string[] = [];
604
+ const [visible, setVisible] = createSignal(true);
605
+
606
+ // Solid.js style: onCleanup directly in component body
607
+ function Child() {
608
+ log.push("component created");
609
+
610
+ // This is the Solid.js pattern - cleanup in component body, not in onMount
611
+ onCleanup(() => {
612
+ log.push("cleanup");
613
+ });
614
+
615
+ return createElement("p", [], [text("I'm here")]);
616
+ }
617
+
618
+ createRoot(() => {
619
+ render(
620
+ container,
621
+ Show({ when: visible, children: () => Child() })
622
+ );
623
+ });
624
+
625
+ // No microtask wait needed - component body runs synchronously
626
+ expect(log).toEqual(["component created"]);
627
+ expect(container.querySelector("p")).not.toBeNull();
628
+
629
+ // Unmount the child
630
+ setVisible(false);
631
+
632
+ expect(log).toEqual(["component created", "cleanup"]);
633
+ expect(container.querySelector("p")).toBeNull();
634
+ });
635
+
636
+ test("timer cleanup pattern (Solid.js style)", async () => {
637
+ const [visible, setVisible] = createSignal(true);
638
+ let timerCleared = false;
639
+
640
+ // Solid.js docs example pattern
641
+ function Timer() {
642
+ const [count, setCount] = createSignal(0);
643
+
644
+ // Setup side effect directly in component
645
+ const interval = setInterval(() => {
646
+ setCount(c => c + 1);
647
+ }, 1000);
648
+
649
+ // Cleanup registered directly in component body
650
+ onCleanup(() => {
651
+ clearInterval(interval);
652
+ timerCleared = true;
653
+ });
654
+
655
+ return createElement("p", [], [textDyn(() => `Count: ${count()}`)]);
656
+ }
657
+
658
+ createRoot(() => {
659
+ render(
660
+ container,
661
+ Show({ when: visible, children: () => Timer() })
662
+ );
663
+ });
664
+
665
+ expect(timerCleared).toBe(false);
666
+
667
+ setVisible(false);
668
+
669
+ expect(timerCleared).toBe(true);
670
+ });
671
+
672
+ test("event listener pattern (Solid.js docs example)", async () => {
673
+ const [visible, setVisible] = createSignal(true);
674
+ const addedListeners: string[] = [];
675
+ const removedListeners: string[] = [];
676
+
677
+ // Mock document.addEventListener/removeEventListener
678
+ const originalAdd = document.addEventListener;
679
+ const originalRemove = document.removeEventListener;
680
+
681
+ document.addEventListener = (type: string) => {
682
+ addedListeners.push(type);
683
+ };
684
+ document.removeEventListener = (type: string) => {
685
+ removedListeners.push(type);
686
+ };
687
+
688
+ // Exact pattern from Solid.js docs
689
+ function ClickCounter() {
690
+ const [count, setCount] = createSignal(0);
691
+ const handleClick = () => setCount(value => value + 1);
692
+
693
+ document.addEventListener("click", handleClick as any);
694
+
695
+ onCleanup(() => {
696
+ document.removeEventListener("click", handleClick as any);
697
+ });
698
+
699
+ return createElement("main", [], [
700
+ textDyn(() => `Document has been clicked ${count()} times`)
701
+ ]);
702
+ }
703
+
704
+ createRoot(() => {
705
+ render(
706
+ container,
707
+ Show({ when: visible, children: () => ClickCounter() })
708
+ );
709
+ });
710
+
711
+ expect(addedListeners).toEqual(["click"]);
712
+ expect(removedListeners).toEqual([]);
713
+
714
+ setVisible(false);
715
+
716
+ expect(removedListeners).toEqual(["click"]);
717
+
718
+ // Restore
719
+ document.addEventListener = originalAdd;
720
+ document.removeEventListener = originalRemove;
721
+ });
722
+
723
+ test("multiple cleanups in component body (LIFO order)", async () => {
724
+ const log: string[] = [];
725
+ const [visible, setVisible] = createSignal(true);
726
+
727
+ function Child() {
728
+ onCleanup(() => log.push("first registered"));
729
+ onCleanup(() => log.push("second registered"));
730
+ onCleanup(() => log.push("third registered"));
731
+
732
+ return createElement("div", [], [text("child")]);
733
+ }
734
+
735
+ createRoot(() => {
736
+ render(
737
+ container,
738
+ Show({ when: visible, children: () => Child() })
739
+ );
740
+ });
741
+
742
+ setVisible(false);
743
+
744
+ // Cleanups run in reverse order (LIFO)
745
+ expect(log).toEqual([
746
+ "third registered",
747
+ "second registered",
748
+ "first registered",
749
+ ]);
750
+ });
751
+
752
+ test("onCleanup works with For loop items (component body style)", async () => {
753
+ const cleanups: string[] = [];
754
+ const [items, setItems] = createSignal(["a", "b", "c"]);
755
+
756
+ function Item({ id }: { id: string }) {
757
+ // Solid.js style - cleanup in component body
758
+ onCleanup(() => {
759
+ cleanups.push(id);
760
+ });
761
+ return createElement("li", [], [text(id)]);
762
+ }
763
+
764
+ createRoot(() => {
765
+ render(
766
+ container,
767
+ createElement("ul", [], [
768
+ For({
769
+ each: items,
770
+ children: (item: string) => Item({ id: item }),
771
+ }),
772
+ ])
773
+ );
774
+ });
775
+
776
+ expect(container.querySelectorAll("li").length).toBe(3);
777
+ expect(cleanups).toEqual([]);
778
+
779
+ // Remove one item
780
+ setItems(["a", "c"]);
781
+
782
+ expect(container.querySelectorAll("li").length).toBe(2);
783
+ expect(cleanups).toContain("b");
784
+ });
785
+
786
+ test("combining onMount and onCleanup in component body", async () => {
787
+ const log: string[] = [];
788
+ const [visible, setVisible] = createSignal(true);
789
+
790
+ function Child() {
791
+ log.push("component body start");
792
+
793
+ // Cleanup in component body (runs on unmount)
794
+ onCleanup(() => {
795
+ log.push("body cleanup");
796
+ });
797
+
798
+ // onMount for DOM-dependent setup
799
+ onMount(() => {
800
+ log.push("mounted");
801
+ // Cleanup inside onMount (also runs on unmount)
802
+ onCleanup(() => {
803
+ log.push("mount cleanup");
804
+ });
805
+ });
806
+
807
+ log.push("component body end");
808
+ return createElement("div", [], [text("content")]);
809
+ }
810
+
811
+ createRoot(() => {
812
+ render(
813
+ container,
814
+ Show({ when: visible, children: () => Child() })
815
+ );
816
+ });
817
+
818
+ await flushMicrotasks();
819
+
820
+ expect(log).toEqual([
821
+ "component body start",
822
+ "component body end",
823
+ "mounted"
824
+ ]);
825
+
826
+ setVisible(false);
827
+ await flushMicrotasks();
828
+
829
+ // Both cleanups should run
830
+ expect(log).toContain("body cleanup");
831
+ expect(log).toContain("mount cleanup");
832
+ });
833
+ });