@luna_ui/luna 0.3.4 → 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 (172) 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.js +1 -1
  9. package/dist/{src-BDdxGwvq.js → src-CHiGeWfy.js} +1 -1
  10. package/dist/vite-plugin.d.ts +122 -0
  11. package/dist/vite-plugin.js +1518 -0
  12. package/package.json +16 -2
  13. package/src/css/extract.ts +798 -0
  14. package/src/css/index.ts +10 -0
  15. package/src/css/inject.ts +205 -0
  16. package/src/css/inline.ts +182 -0
  17. package/src/css/minify.ts +70 -0
  18. package/src/css/optimizer.ts +6 -0
  19. package/src/css/runtime.ts +344 -0
  20. package/src/css-optimizer/README.md +353 -0
  21. package/src/css-optimizer/cooccurrence.ts +100 -0
  22. package/src/css-optimizer/core.ts +263 -0
  23. package/src/css-optimizer/extractors.ts +243 -0
  24. package/src/css-optimizer/hash.ts +54 -0
  25. package/src/css-optimizer/index.ts +129 -0
  26. package/src/css-optimizer/merge.ts +109 -0
  27. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  28. package/src/css-optimizer/parser.ts +120 -0
  29. package/src/css-optimizer/pattern.ts +171 -0
  30. package/src/css-optimizer/transformers.ts +301 -0
  31. package/src/css-optimizer/types.ts +128 -0
  32. package/src/event-utils.ts +227 -0
  33. package/src/index.ts +890 -0
  34. package/src/jsx-dev-runtime.ts +2 -0
  35. package/src/jsx-runtime.ts +398 -0
  36. package/src/vite-plugin.ts +718 -0
  37. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  38. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  39. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  40. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  41. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  42. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  43. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  44. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  45. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  46. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  50. 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
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  64. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  65. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  66. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  67. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  68. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  69. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  70. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  71. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  72. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  73. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  74. 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
  75. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  76. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  77. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  117. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  118. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  119. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  120. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  121. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  122. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  123. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  124. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  125. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  137. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  138. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  139. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  140. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  141. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  142. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  143. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  144. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  145. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  147. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  148. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  149. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  150. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  151. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  152. package/tests/context.test.ts +118 -0
  153. package/tests/css-optimizer-extractors.test.ts +264 -0
  154. package/tests/css-optimizer-integration.test.ts +566 -0
  155. package/tests/css-optimizer-transformers.test.ts +301 -0
  156. package/tests/css-optimizer.test.ts +646 -0
  157. package/tests/css-runtime.bench.ts +442 -0
  158. package/tests/css-runtime.test.ts +342 -0
  159. package/tests/dom.test.ts +872 -0
  160. package/tests/integration.test.ts +405 -0
  161. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  162. package/tests/jsx-runtime.test.tsx +393 -0
  163. package/tests/lifecycle.test.ts +833 -0
  164. package/tests/move-before.bench.ts +304 -0
  165. package/tests/preact-signals-comparison.test.ts +1608 -0
  166. package/tests/resource.test.ts +160 -0
  167. package/tests/router.test.ts +117 -0
  168. package/tests/show-initial-mount-leak.test.tsx +182 -0
  169. package/tests/solidjs-api.test.ts +659 -0
  170. package/tests/static-perf.bench.ts +64 -0
  171. package/tests/store.test.ts +263 -0
  172. package/tests/tsx-syntax.test.tsx +404 -0
@@ -0,0 +1,263 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import {
3
+ createStore,
4
+ createRenderEffect,
5
+ createRoot,
6
+ produce,
7
+ reconcile,
8
+ } from "../src/index";
9
+
10
+ describe("createStore", () => {
11
+ describe("Basic functionality", () => {
12
+ test("creates a store with initial value", () => {
13
+ const [state] = createStore({ count: 0 });
14
+ expect(state.count).toBe(0);
15
+ });
16
+
17
+ test("nested property access works", () => {
18
+ const [state] = createStore({
19
+ user: { name: "John", age: 30 },
20
+ });
21
+ expect(state.user.name).toBe("John");
22
+ expect(state.user.age).toBe(30);
23
+ });
24
+
25
+ test("deeply nested access works", () => {
26
+ const [state] = createStore({
27
+ a: { b: { c: { d: 42 } } },
28
+ });
29
+ expect(state.a.b.c.d).toBe(42);
30
+ });
31
+ });
32
+
33
+ describe("setState with path", () => {
34
+ test("updates single property", () => {
35
+ const [state, setState] = createStore({ count: 0 });
36
+ setState("count", 1);
37
+ expect(state.count).toBe(1);
38
+ });
39
+
40
+ test("updates nested property", () => {
41
+ const [state, setState] = createStore({
42
+ user: { name: "John" },
43
+ });
44
+ setState("user", "name", "Jane");
45
+ expect(state.user.name).toBe("Jane");
46
+ });
47
+
48
+ test("functional update works", () => {
49
+ const [state, setState] = createStore({ count: 5 });
50
+ setState("count", (c: number) => c + 1);
51
+ expect(state.count).toBe(6);
52
+ });
53
+
54
+ test("merges objects at path", () => {
55
+ const [state, setState] = createStore({
56
+ user: { name: "John", age: 30 },
57
+ });
58
+ setState("user", { age: 31 });
59
+ expect(state.user.name).toBe("John");
60
+ expect(state.user.age).toBe(31);
61
+ });
62
+
63
+ test("merges at root level", () => {
64
+ const [state, setState] = createStore({
65
+ count: 0,
66
+ name: "test",
67
+ });
68
+ setState({ count: 10 });
69
+ expect(state.count).toBe(10);
70
+ expect(state.name).toBe("test");
71
+ });
72
+ });
73
+
74
+ describe("Reactivity", () => {
75
+ test("tracks property access in effects", () => {
76
+ createRoot(() => {
77
+ const [state, setState] = createStore({ count: 0 });
78
+ const values: number[] = [];
79
+
80
+ createRenderEffect(() => {
81
+ values.push(state.count);
82
+ });
83
+
84
+ expect(values).toEqual([0]);
85
+
86
+ setState("count", 1);
87
+ expect(values).toEqual([0, 1]);
88
+
89
+ setState("count", 2);
90
+ expect(values).toEqual([0, 1, 2]);
91
+ });
92
+ });
93
+
94
+ test("tracks nested property access", () => {
95
+ createRoot(() => {
96
+ const [state, setState] = createStore({
97
+ user: { name: "John" },
98
+ });
99
+ const names: string[] = [];
100
+
101
+ createRenderEffect(() => {
102
+ names.push(state.user.name);
103
+ });
104
+
105
+ expect(names).toEqual(["John"]);
106
+
107
+ setState("user", "name", "Jane");
108
+ expect(names).toEqual(["John", "Jane"]);
109
+ });
110
+ });
111
+
112
+ test("only triggers when accessed property changes", () => {
113
+ createRoot(() => {
114
+ const [state, setState] = createStore({
115
+ a: 1,
116
+ b: 2,
117
+ });
118
+ const aValues: number[] = [];
119
+
120
+ createRenderEffect(() => {
121
+ aValues.push(state.a);
122
+ });
123
+
124
+ expect(aValues).toEqual([1]);
125
+
126
+ // Changing b should not trigger effect that only reads a
127
+ setState("b", 3);
128
+ expect(aValues).toEqual([1]);
129
+
130
+ // Changing a should trigger
131
+ setState("a", 10);
132
+ expect(aValues).toEqual([1, 10]);
133
+ });
134
+ });
135
+
136
+ test("parent path change notifies child accessors", () => {
137
+ createRoot(() => {
138
+ const [state, setState] = createStore({
139
+ user: { name: "John", age: 30 },
140
+ });
141
+ const names: string[] = [];
142
+
143
+ createRenderEffect(() => {
144
+ names.push(state.user.name);
145
+ });
146
+
147
+ expect(names).toEqual(["John"]);
148
+
149
+ // Replacing the entire user object should trigger
150
+ setState("user", { name: "Jane", age: 25 });
151
+ expect(names).toEqual(["John", "Jane"]);
152
+ });
153
+ });
154
+ });
155
+
156
+ describe("Arrays", () => {
157
+ test("array access works", () => {
158
+ const [state] = createStore({ items: [1, 2, 3] });
159
+ expect(state.items[0]).toBe(1);
160
+ expect(state.items.length).toBe(3);
161
+ });
162
+
163
+ test("array updates work", () => {
164
+ const [state, setState] = createStore({ items: ["a", "b", "c"] });
165
+ setState("items", [...state.items, "d"]);
166
+ expect(state.items).toEqual(["a", "b", "c", "d"]);
167
+ });
168
+
169
+ test("array with objects", () => {
170
+ const [state, setState] = createStore({
171
+ todos: [
172
+ { id: 1, text: "First", done: false },
173
+ { id: 2, text: "Second", done: true },
174
+ ],
175
+ });
176
+
177
+ expect(state.todos[0].text).toBe("First");
178
+ expect(state.todos[1].done).toBe(true);
179
+ });
180
+ });
181
+
182
+ describe("produce helper", () => {
183
+ test("produce creates a mutation function", () => {
184
+ const [state, setState] = createStore({
185
+ user: { name: "John", age: 30 },
186
+ });
187
+
188
+ setState(
189
+ "user",
190
+ produce((user: { name: string; age: number }) => {
191
+ user.name = "Jane";
192
+ user.age = 31;
193
+ })
194
+ );
195
+
196
+ expect(state.user.name).toBe("Jane");
197
+ expect(state.user.age).toBe(31);
198
+ });
199
+
200
+ test("produce works with arrays", () => {
201
+ const [state, setState] = createStore({
202
+ items: [1, 2, 3],
203
+ });
204
+
205
+ setState(
206
+ "items",
207
+ produce((items: number[]) => {
208
+ items.push(4);
209
+ })
210
+ );
211
+
212
+ expect(state.items).toEqual([1, 2, 3, 4]);
213
+ });
214
+ });
215
+
216
+ describe("reconcile helper", () => {
217
+ test("reconcile replaces entire value", () => {
218
+ const [state, setState] = createStore({
219
+ items: [1, 2, 3],
220
+ });
221
+
222
+ setState("items", reconcile([4, 5, 6]));
223
+ expect(state.items).toEqual([4, 5, 6]);
224
+ });
225
+ });
226
+
227
+ describe("Edge cases", () => {
228
+ test("handles undefined values", () => {
229
+ const [state, setState] = createStore<{
230
+ value: number | undefined;
231
+ }>({ value: undefined });
232
+ expect(state.value).toBeUndefined();
233
+
234
+ setState("value", 42);
235
+ expect(state.value).toBe(42);
236
+
237
+ setState("value", undefined);
238
+ expect(state.value).toBeUndefined();
239
+ });
240
+
241
+ test("handles null values", () => {
242
+ const [state, setState] = createStore<{
243
+ value: string | null;
244
+ }>({ value: null });
245
+ expect(state.value).toBeNull();
246
+
247
+ setState("value", "hello");
248
+ expect(state.value).toBe("hello");
249
+
250
+ setState("value", null);
251
+ expect(state.value).toBeNull();
252
+ });
253
+
254
+ test("creates nested paths that don't exist", () => {
255
+ const [state, setState] = createStore<{
256
+ deep?: { nested?: { value?: number } };
257
+ }>({});
258
+
259
+ setState("deep", "nested", "value", 42);
260
+ expect(state.deep?.nested?.value).toBe(42);
261
+ });
262
+ });
263
+ });
@@ -0,0 +1,404 @@
1
+ // Test actual TSX syntax with jsxImportSource
2
+ import { describe, test, expect } from "vitest";
3
+ import "global-jsdom/register";
4
+ import { createSignal, createRoot, render, Fragment, Show, Switch, Match } from "../src/index";
5
+ import type { JSX } from "../src/jsx-runtime";
6
+
7
+ describe("TSX Syntax with jsxImportSource", () => {
8
+ test("basic element", () => {
9
+ const node = <div className="test">Hello</div>;
10
+ expect(node).toBeDefined();
11
+ });
12
+
13
+ test("nested elements", () => {
14
+ const node = (
15
+ <div className="container">
16
+ <h1>Title</h1>
17
+ <p>Paragraph</p>
18
+ </div>
19
+ );
20
+ expect(node).toBeDefined();
21
+ });
22
+
23
+ test("element with multiple attributes", () => {
24
+ const node = (
25
+ <input
26
+ type="text"
27
+ placeholder="Enter name"
28
+ value="initial"
29
+ />
30
+ );
31
+ expect(node).toBeDefined();
32
+ });
33
+
34
+ test("event handler", () => {
35
+ let clicked = false;
36
+ const node = (
37
+ <button onClick={() => { clicked = true; }}>
38
+ Click me
39
+ </button>
40
+ );
41
+ expect(node).toBeDefined();
42
+ });
43
+
44
+ test("function component", () => {
45
+ function Greeting(props: { name: string }): JSX.Element {
46
+ return <span>Hello, {props.name}!</span>;
47
+ }
48
+
49
+ const node = <Greeting name="World" />;
50
+ expect(node).toBeDefined();
51
+ });
52
+
53
+ test("component with children", () => {
54
+ function Card(props: { title: string; children?: JSX.Element }): JSX.Element {
55
+ return (
56
+ <div className="card">
57
+ <h2>{props.title}</h2>
58
+ <div className="content">{props.children}</div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ const node = (
64
+ <Card title="My Card">
65
+ <p>Card content here</p>
66
+ </Card>
67
+ );
68
+ expect(node).toBeDefined();
69
+ });
70
+
71
+ test("fragment with shorthand syntax", () => {
72
+ const node = (
73
+ <>
74
+ <div>First</div>
75
+ <div>Second</div>
76
+ </>
77
+ );
78
+ // Fragment should return a DomNode (not an array)
79
+ expect(node).toBeDefined();
80
+ expect(Array.isArray(node)).toBe(false);
81
+ });
82
+
83
+ test("Fragment component with explicit JSX tag", () => {
84
+ // This is the bug fix test: <Fragment> should work like <>
85
+ const node = (
86
+ <Fragment>
87
+ <div>First</div>
88
+ <div>Second</div>
89
+ </Fragment>
90
+ );
91
+ expect(node).toBeDefined();
92
+ expect(Array.isArray(node)).toBe(false);
93
+ });
94
+
95
+ test("Fragment renders correctly to DOM", () => {
96
+ const container = document.createElement("div");
97
+
98
+ const node = (
99
+ <div id="wrapper">
100
+ <Fragment>
101
+ <span>A</span>
102
+ <span>B</span>
103
+ </Fragment>
104
+ </div>
105
+ );
106
+
107
+ render(container, node);
108
+
109
+ expect(container.innerHTML).toContain("A");
110
+ expect(container.innerHTML).toContain("B");
111
+ // Both spans should be direct children of wrapper
112
+ const wrapper = container.querySelector("#wrapper");
113
+ expect(wrapper?.querySelectorAll("span").length).toBe(2);
114
+ });
115
+
116
+ test("component returning Fragment works", () => {
117
+ function MultipleElements(): JSX.Element {
118
+ return (
119
+ <Fragment>
120
+ <div>One</div>
121
+ <div>Two</div>
122
+ <div>Three</div>
123
+ </Fragment>
124
+ );
125
+ }
126
+
127
+ const container = document.createElement("div");
128
+ render(container, <MultipleElements />);
129
+
130
+ expect(container.textContent).toContain("One");
131
+ expect(container.textContent).toContain("Two");
132
+ expect(container.textContent).toContain("Three");
133
+ });
134
+
135
+ test("render TSX to DOM", () => {
136
+ const container = document.createElement("div");
137
+
138
+ const node = (
139
+ <div id="app">
140
+ <h1 className="title">Hello TSX</h1>
141
+ <p>This is a paragraph.</p>
142
+ </div>
143
+ );
144
+
145
+ render(container, node);
146
+
147
+ expect(container.innerHTML).toContain("app");
148
+ expect(container.innerHTML).toContain("title");
149
+ expect(container.innerHTML).toContain("Hello TSX");
150
+ expect(container.innerHTML).toContain("paragraph");
151
+ });
152
+
153
+ test("reactive value with signal", () => {
154
+ const [count] = createSignal(0);
155
+
156
+ const node = (
157
+ <div className={() => `count-${count()}`}>
158
+ Count value
159
+ </div>
160
+ );
161
+
162
+ expect(node).toBeDefined();
163
+ });
164
+
165
+ test("list rendering", () => {
166
+ const items = ["Apple", "Banana", "Cherry"];
167
+
168
+ const node = (
169
+ <ul>
170
+ {items.map((item) => (
171
+ <li>{item}</li>
172
+ ))}
173
+ </ul>
174
+ );
175
+
176
+ expect(node).toBeDefined();
177
+ });
178
+
179
+ test("conditional rendering in component", () => {
180
+ function Toggle(props: { show: boolean }): JSX.Element {
181
+ return props.show ? <div>Visible</div> : <div>Hidden</div>;
182
+ }
183
+
184
+ const visible = <Toggle show={true} />;
185
+ const hidden = <Toggle show={false} />;
186
+
187
+ expect(visible).toBeDefined();
188
+ expect(hidden).toBeDefined();
189
+ });
190
+
191
+ test("Show with function children returning array", () => {
192
+ const container = document.createElement("div");
193
+
194
+ createRoot((dispose) => {
195
+ const [show, setShow] = createSignal(true);
196
+
197
+ // Function children that returns multiple elements (array)
198
+ const node = (
199
+ <div id="show-test">
200
+ <Show when={show}>
201
+ {(value) => [
202
+ <span>Shown: {String(value)}</span>,
203
+ <span>Also shown</span>
204
+ ]}
205
+ </Show>
206
+ </div>
207
+ );
208
+
209
+ render(container, node);
210
+
211
+ expect(container.textContent).toContain("Shown");
212
+ expect(container.textContent).toContain("Also shown");
213
+
214
+ dispose();
215
+ });
216
+ });
217
+
218
+ test("Switch/Match with function children", () => {
219
+ const container = document.createElement("div");
220
+
221
+ createRoot((dispose) => {
222
+ const [value, setValue] = createSignal("a");
223
+
224
+ const node = (
225
+ <div id="switch-test">
226
+ <Switch fallback={<span>No match</span>}>
227
+ <Match when={() => value() === "a"}>
228
+ {() => <span>Match A</span>}
229
+ </Match>
230
+ <Match when={() => value() === "b"}>
231
+ {() => [
232
+ <span>Match B1</span>,
233
+ <span>Match B2</span>
234
+ ]}
235
+ </Match>
236
+ </Switch>
237
+ </div>
238
+ );
239
+
240
+ render(container, node);
241
+ expect(container.textContent).toContain("Match A");
242
+
243
+ setValue("b");
244
+ expect(container.textContent).toContain("Match B1");
245
+ expect(container.textContent).toContain("Match B2");
246
+
247
+ setValue("c");
248
+ expect(container.textContent).toContain("No match");
249
+
250
+ dispose();
251
+ });
252
+ });
253
+
254
+ test("Switch/Match with direct multiple children (no function wrapper)", () => {
255
+ const container = document.createElement("div");
256
+
257
+ createRoot((dispose) => {
258
+ const [isEven, setIsEven] = createSignal(true);
259
+
260
+ const node = (
261
+ <div id="direct-children-test">
262
+ <Switch fallback={<span>Odd</span>}>
263
+ <Match when={() => isEven()}>
264
+ <div>A</div>
265
+ <div>B</div>
266
+ </Match>
267
+ </Switch>
268
+ </div>
269
+ );
270
+
271
+ render(container, node);
272
+ expect(container.textContent).toContain("A");
273
+ expect(container.textContent).toContain("B");
274
+
275
+ setIsEven(false);
276
+ expect(container.textContent).toContain("Odd");
277
+ expect(container.textContent).not.toContain("A");
278
+ expect(container.textContent).not.toContain("B");
279
+
280
+ dispose();
281
+ });
282
+ });
283
+
284
+ // Issue #7: Reactive props (class, innerHTML)
285
+ describe("Reactive props (Issue #7)", () => {
286
+ test("reactive class with accessor function", () => {
287
+ const container = document.createElement("div");
288
+
289
+ createRoot((dispose) => {
290
+ const [active, setActive] = createSignal(false);
291
+
292
+ // Pass accessor function directly (not called): class={className}
293
+ const className = () => `btn ${active() ? "active" : "inactive"}`;
294
+
295
+ const node = (
296
+ <button class={className}>
297
+ Toggle
298
+ </button>
299
+ );
300
+
301
+ render(container, node);
302
+ const button = container.querySelector("button")!;
303
+
304
+ // Initial state
305
+ expect(button.className).toBe("btn inactive");
306
+
307
+ // After update
308
+ setActive(true);
309
+ expect(button.className).toBe("btn active");
310
+
311
+ // Toggle back
312
+ setActive(false);
313
+ expect(button.className).toBe("btn inactive");
314
+
315
+ dispose();
316
+ });
317
+ });
318
+
319
+ test("reactive className with accessor function", () => {
320
+ const container = document.createElement("div");
321
+
322
+ createRoot((dispose) => {
323
+ const [color, setColor] = createSignal("red");
324
+
325
+ const node = (
326
+ <div className={() => `text-${color()}`}>
327
+ Colored text
328
+ </div>
329
+ );
330
+
331
+ render(container, node);
332
+ const div = container.querySelector("div")!;
333
+
334
+ expect(div.className).toBe("text-red");
335
+
336
+ setColor("blue");
337
+ expect(div.className).toBe("text-blue");
338
+
339
+ dispose();
340
+ });
341
+ });
342
+
343
+ test("dangerouslySetInnerHTML static", () => {
344
+ const container = document.createElement("div");
345
+
346
+ const node = (
347
+ <span dangerouslySetInnerHTML={{ __html: "<b>bold</b>" }} />
348
+ );
349
+
350
+ render(container, node);
351
+ const span = container.querySelector("span")!;
352
+
353
+ expect(span.innerHTML).toBe("<b>bold</b>");
354
+ expect(span.querySelector("b")?.textContent).toBe("bold");
355
+ });
356
+
357
+ test("dangerouslySetInnerHTML dynamic", () => {
358
+ const container = document.createElement("div");
359
+
360
+ createRoot((dispose) => {
361
+ const [html, setHtml] = createSignal("<i>italic</i>");
362
+
363
+ const node = (
364
+ <span dangerouslySetInnerHTML={() => ({ __html: html() })} />
365
+ );
366
+
367
+ render(container, node);
368
+ const span = container.querySelector("span")!;
369
+
370
+ expect(span.innerHTML).toBe("<i>italic</i>");
371
+
372
+ setHtml("<strong>strong</strong>");
373
+ expect(span.innerHTML).toBe("<strong>strong</strong>");
374
+
375
+ dispose();
376
+ });
377
+ });
378
+
379
+ test("reactive style object", () => {
380
+ const container = document.createElement("div");
381
+
382
+ createRoot((dispose) => {
383
+ const [color, setColor] = createSignal("red");
384
+
385
+ const node = (
386
+ <div style={() => ({ color: color(), fontWeight: "bold" })}>
387
+ Styled text
388
+ </div>
389
+ );
390
+
391
+ render(container, node);
392
+ const div = container.querySelector("div")!;
393
+
394
+ expect(div.getAttribute("style")).toContain("color: red");
395
+ expect(div.getAttribute("style")).toContain("font-weight: bold");
396
+
397
+ setColor("blue");
398
+ expect(div.getAttribute("style")).toContain("color: blue");
399
+
400
+ dispose();
401
+ });
402
+ });
403
+ });
404
+ });