ripple 0.2.215 → 0.3.0

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 (157) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/package.json +16 -7
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/identifier-utils.js +2 -0
  5. package/src/compiler/index.d.ts +2 -6
  6. package/src/compiler/phases/1-parse/index.js +171 -233
  7. package/src/compiler/phases/2-analyze/index.js +216 -16
  8. package/src/compiler/phases/2-analyze/prune.js +2 -2
  9. package/src/compiler/phases/3-transform/client/index.js +326 -94
  10. package/src/compiler/phases/3-transform/segments.js +43 -15
  11. package/src/compiler/phases/3-transform/server/index.js +71 -21
  12. package/src/compiler/scope.js +31 -12
  13. package/src/compiler/source-map-utils.js +4 -6
  14. package/src/compiler/types/acorn.d.ts +11 -0
  15. package/src/compiler/types/estree-jsx.d.ts +11 -0
  16. package/src/compiler/types/estree.d.ts +11 -0
  17. package/src/compiler/types/import.d.ts +32 -18
  18. package/src/compiler/types/index.d.ts +75 -23
  19. package/src/compiler/types/parse.d.ts +7 -10
  20. package/src/compiler/utils.js +48 -0
  21. package/src/runtime/array.js +53 -22
  22. package/src/runtime/date.js +15 -5
  23. package/src/runtime/index-client.js +41 -7
  24. package/src/runtime/index-server.js +7 -7
  25. package/src/runtime/internal/client/bindings.js +2 -2
  26. package/src/runtime/internal/client/blocks.js +40 -1
  27. package/src/runtime/internal/client/context.js +8 -0
  28. package/src/runtime/internal/client/for.js +3 -3
  29. package/src/runtime/internal/client/index.js +32 -5
  30. package/src/runtime/internal/client/render.js +20 -8
  31. package/src/runtime/internal/client/runtime.js +9 -7
  32. package/src/runtime/internal/client/template.js +1 -1
  33. package/src/runtime/internal/client/try.js +15 -22
  34. package/src/runtime/internal/client/utils.js +1 -1
  35. package/src/runtime/internal/server/context.js +8 -0
  36. package/src/runtime/internal/server/index.js +99 -6
  37. package/src/runtime/map.js +7 -7
  38. package/src/runtime/media-query.js +10 -1
  39. package/src/runtime/object.js +6 -6
  40. package/src/runtime/proxy.js +6 -6
  41. package/src/runtime/set.js +11 -11
  42. package/src/runtime/url-search-params.js +13 -2
  43. package/src/runtime/url.js +15 -5
  44. package/src/utils/builders.js +13 -3
  45. package/tests/client/array/array.copy-within.test.ripple +11 -11
  46. package/tests/client/array/array.derived.test.ripple +42 -42
  47. package/tests/client/array/array.iteration.test.ripple +12 -12
  48. package/tests/client/array/array.mutations.test.ripple +25 -25
  49. package/tests/client/array/array.static.test.ripple +103 -106
  50. package/tests/client/array/array.to-methods.test.ripple +8 -8
  51. package/tests/client/async-suspend.test.ripple +94 -0
  52. package/tests/client/basic/basic.attributes.test.ripple +31 -31
  53. package/tests/client/basic/basic.collections.test.ripple +7 -7
  54. package/tests/client/basic/basic.components.test.ripple +48 -10
  55. package/tests/client/basic/basic.errors.test.ripple +111 -30
  56. package/tests/client/basic/basic.events.test.ripple +11 -11
  57. package/tests/client/basic/basic.get-set.test.ripple +18 -18
  58. package/tests/client/basic/basic.reactivity.test.ripple +47 -42
  59. package/tests/client/basic/basic.rendering.test.ripple +7 -7
  60. package/tests/client/basic/basic.utilities.test.ripple +4 -4
  61. package/tests/client/boundaries.test.ripple +7 -7
  62. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +2 -2
  63. package/tests/client/compiler/compiler.assignments.test.ripple +21 -21
  64. package/tests/client/compiler/compiler.basic.test.ripple +223 -82
  65. package/tests/client/compiler/compiler.tracked-access.test.ripple +8 -9
  66. package/tests/client/composite/composite.dynamic-components.test.ripple +8 -8
  67. package/tests/client/composite/composite.generics.test.ripple +4 -4
  68. package/tests/client/composite/composite.props.test.ripple +9 -9
  69. package/tests/client/composite/composite.reactivity.test.ripple +32 -26
  70. package/tests/client/composite/composite.render.test.ripple +13 -4
  71. package/tests/client/computed-properties.test.ripple +3 -3
  72. package/tests/client/context.test.ripple +3 -3
  73. package/tests/client/css/global-additional-cases.test.ripple +4 -4
  74. package/tests/client/css/style-identifier.test.ripple +49 -41
  75. package/tests/client/date.test.ripple +40 -40
  76. package/tests/client/dynamic-elements.test.ripple +165 -30
  77. package/tests/client/events.test.ripple +25 -25
  78. package/tests/client/for.test.ripple +76 -8
  79. package/tests/client/function-overload.test.ripple +0 -1
  80. package/tests/client/head.test.ripple +7 -7
  81. package/tests/client/html.test.ripple +2 -2
  82. package/tests/client/input-value.test.ripple +174 -176
  83. package/tests/client/map.test.ripple +21 -21
  84. package/tests/client/media-query.test.ripple +4 -4
  85. package/tests/client/object.test.ripple +12 -12
  86. package/tests/client/portal.test.ripple +4 -4
  87. package/tests/client/ref.test.ripple +5 -5
  88. package/tests/client/return.test.ripple +17 -17
  89. package/tests/client/set.test.ripple +16 -16
  90. package/tests/client/svg.test.ripple +6 -7
  91. package/tests/client/switch.test.ripple +10 -10
  92. package/tests/client/tracked-expression.test.ripple +1 -3
  93. package/tests/client/try.test.ripple +56 -4
  94. package/tests/client/url/url.derived.test.ripple +10 -9
  95. package/tests/client/url/url.parsing.test.ripple +10 -10
  96. package/tests/client/url/url.partial-removal.test.ripple +10 -10
  97. package/tests/client/url/url.reactivity.test.ripple +17 -17
  98. package/tests/client/url/url.serialization.test.ripple +4 -4
  99. package/tests/client/url-search-params/url-search-params.derived.test.ripple +11 -10
  100. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +5 -7
  101. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +13 -13
  102. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +19 -19
  103. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +17 -17
  104. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +5 -5
  105. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +5 -5
  106. package/tests/hydration/compiled/client/events.js +8 -11
  107. package/tests/hydration/compiled/client/for.js +20 -23
  108. package/tests/hydration/compiled/client/head.js +17 -19
  109. package/tests/hydration/compiled/client/hmr.js +84 -0
  110. package/tests/hydration/compiled/client/html.js +1 -15
  111. package/tests/hydration/compiled/client/if-children.js +7 -9
  112. package/tests/hydration/compiled/client/if.js +5 -7
  113. package/tests/hydration/compiled/client/mixed-control-flow.js +3 -5
  114. package/tests/hydration/compiled/client/portal.js +1 -1
  115. package/tests/hydration/compiled/client/reactivity.js +9 -11
  116. package/tests/hydration/compiled/client/return.js +11 -13
  117. package/tests/hydration/compiled/client/switch.js +4 -6
  118. package/tests/hydration/compiled/server/basic.js +0 -1
  119. package/tests/hydration/compiled/server/composite.js +0 -3
  120. package/tests/hydration/compiled/server/events.js +8 -12
  121. package/tests/hydration/compiled/server/for.js +20 -23
  122. package/tests/hydration/compiled/server/head.js +17 -19
  123. package/tests/hydration/compiled/server/hmr.js +107 -0
  124. package/tests/hydration/compiled/server/html.js +1 -35
  125. package/tests/hydration/compiled/server/if-children.js +7 -11
  126. package/tests/hydration/compiled/server/if.js +5 -7
  127. package/tests/hydration/compiled/server/mixed-control-flow.js +3 -5
  128. package/tests/hydration/compiled/server/portal.js +1 -9
  129. package/tests/hydration/compiled/server/reactivity.js +9 -11
  130. package/tests/hydration/compiled/server/return.js +11 -13
  131. package/tests/hydration/compiled/server/switch.js +4 -6
  132. package/tests/hydration/components/events.ripple +8 -9
  133. package/tests/hydration/components/for.ripple +20 -21
  134. package/tests/hydration/components/head.ripple +6 -8
  135. package/tests/hydration/components/hmr.ripple +34 -0
  136. package/tests/hydration/components/html.ripple +1 -3
  137. package/tests/hydration/components/if-children.ripple +7 -8
  138. package/tests/hydration/components/if.ripple +5 -6
  139. package/tests/hydration/components/mixed-control-flow.ripple +4 -6
  140. package/tests/hydration/components/portal.ripple +1 -1
  141. package/tests/hydration/components/reactivity.ripple +9 -10
  142. package/tests/hydration/components/return.ripple +11 -12
  143. package/tests/hydration/components/switch.ripple +6 -8
  144. package/tests/hydration/hmr.test.js +74 -0
  145. package/tests/server/await.test.ripple +2 -2
  146. package/tests/server/basic.attributes.test.ripple +19 -21
  147. package/tests/server/basic.components.test.ripple +13 -7
  148. package/tests/server/basic.test.ripple +20 -21
  149. package/tests/server/compiler.test.ripple +5 -5
  150. package/tests/server/composite.props.test.ripple +6 -7
  151. package/tests/server/composite.test.ripple +4 -4
  152. package/tests/server/context.test.ripple +1 -3
  153. package/tests/server/dynamic-elements.test.ripple +24 -24
  154. package/tests/server/head.test.ripple +5 -7
  155. package/tests/server/style-identifier.test.ripple +16 -17
  156. package/types/index.d.ts +266 -62
  157. package/types/server.d.ts +6 -6
@@ -0,0 +1,34 @@
1
+ // Components for testing HMR re-render after hydration.
2
+ // The key scenario: a layout wrapper whose root element contains children
3
+ // that use if/for blocks. After hydration, hydrate_node can be left pointing
4
+ // deep inside the layout's root element (from nested block processing), which
5
+ // previously caused branch.s.end to be set incorrectly and target to be null.
6
+
7
+ // A layout component similar to docs-layout: a root div wrapping child components
8
+ // where the children contain conditional content (if blocks)
9
+ export component Layout({ children }: { children: any }) {
10
+ <div class="layout">
11
+ <nav class="nav">{'Navigation'}</nav>
12
+ <main class="main">
13
+ <children />
14
+ </main>
15
+ </div>
16
+ }
17
+
18
+ // A child component with an if block (the key source of deep hydrate_node)
19
+ export component Content() {
20
+ let visible = #ripple.track(true);
21
+
22
+ <div class="content">
23
+ if (@visible) {
24
+ <p class="text">{'Hello world'}</p>
25
+ }
26
+ </div>
27
+ }
28
+
29
+ // The top-level component combining Layout + Content (mimics docs layout + page)
30
+ export component LayoutWithContent() {
31
+ <Layout>
32
+ <Content />
33
+ </Layout>
34
+ }
@@ -1,5 +1,3 @@
1
- import { track } from 'ripple';
2
-
3
1
  export component StaticHtml() {
4
2
  const html = '<p><strong>Bold</strong> text</p>';
5
3
  <div>{html html}</div>
@@ -229,7 +227,7 @@ component NavItem({
229
227
  }
230
228
 
231
229
  component SidebarSection({ title, children }: { title: string; children: any }) {
232
- let expanded = track(true);
230
+ let expanded = #ripple.track(true);
233
231
  <section class="sidebar-section">
234
232
  <div class="section-header">
235
233
  <h2>{title}</h2>
@@ -1,9 +1,8 @@
1
1
  // Minimal repro for hydration issue with if block containing children
2
2
  // Based on SidebarGroup pattern from website-new
3
- import { track } from 'ripple';
4
3
 
5
4
  export component IfWithChildren({ children }: { children: any }) {
6
- let expanded = track(true);
5
+ let expanded = #ripple.track(true);
7
6
 
8
7
  <div class="container">
9
8
  <div class="header" role="button" onClick={() => (@expanded = !@expanded)}>{'Toggle'}</div>
@@ -28,7 +27,7 @@ export component TestIfWithChildren() {
28
27
 
29
28
  // Simpler variant - if block with static children
30
29
  export component IfWithStaticChildren() {
31
- let expanded = track(true);
30
+ let expanded = #ripple.track(true);
32
31
 
33
32
  <div class="container">
34
33
  <div class="header" role="button" onClick={() => (@expanded = !@expanded)}>{'Toggle'}</div>
@@ -43,7 +42,7 @@ export component IfWithStaticChildren() {
43
42
 
44
43
  // Variant with sibling elements before the if block (like SidebarGroup)
45
44
  export component IfWithSiblingsAndChildren({ children }: { children: any }) {
46
- let expanded = track(true);
45
+ let expanded = #ripple.track(true);
47
46
 
48
47
  <section class="group">
49
48
  <div class="item" role="button" onClick={() => (@expanded = !@expanded)}>
@@ -74,7 +73,7 @@ export component TestIfWithSiblingsAndChildren() {
74
73
  // This tests that hydrate_node is properly restored after processing an element's children
75
74
  // before navigating to a dynamic sibling (if/for/switch)
76
75
  export component ElementWithChildrenThenIf() {
77
- let show = track(true);
76
+ let show = #ripple.track(true);
78
77
 
79
78
  <div class="wrapper">
80
79
  <div class="nested-parent">
@@ -92,7 +91,7 @@ export component ElementWithChildrenThenIf() {
92
91
 
93
92
  // More complex: multiple levels of nesting before if sibling
94
93
  export component DeepNestingThenIf() {
95
- let visible = track(true);
94
+ let visible = #ripple.track(true);
96
95
 
97
96
  <section class="outer">
98
97
  <article class="middle">
@@ -115,7 +114,7 @@ export component DeepNestingThenIf() {
115
114
  // followed by another sibling element. This requires pop() to restore hydrate_node
116
115
  // because we descend into the first element to get the button children.
117
116
  export component DomElementChildrenThenSibling() {
118
- let activeTab = track('code');
117
+ let activeTab = #ripple.track('code');
119
118
 
120
119
  <div class="tabs">
121
120
  <div class="tab-list">
@@ -148,7 +147,7 @@ export component DomElementChildrenThenSibling() {
148
147
  // generate sibling() calls. This was causing incorrect pop() generation before next().
149
148
  // Pattern: <ul> with dynamic <li> children -> static <h2> -> static <p> -> next()
150
149
  export component DomChildrenThenStaticSiblings() {
151
- let count = track(0);
150
+ let count = #ripple.track(0);
152
151
 
153
152
  <div class="container">
154
153
  <ul class="list">
@@ -1,5 +1,4 @@
1
1
  // If block components for hydration testing
2
- import { track } from 'ripple';
3
2
 
4
3
  export component IfTruthy() {
5
4
  const show = true;
@@ -25,7 +24,7 @@ export component IfElse() {
25
24
  }
26
25
 
27
26
  export component ReactiveIf() {
28
- let show = track(true);
27
+ let show = #ripple.track(true);
29
28
  <button
30
29
  class="toggle"
31
30
  onClick={() => {
@@ -40,7 +39,7 @@ export component ReactiveIf() {
40
39
  }
41
40
 
42
41
  export component ReactiveIfElse() {
43
- let isOn = track(false);
42
+ let isOn = #ripple.track(false);
44
43
  <button
45
44
  class="toggle"
46
45
  onClick={() => {
@@ -57,8 +56,8 @@ export component ReactiveIfElse() {
57
56
  }
58
57
 
59
58
  export component NestedIf() {
60
- let outer = track(true);
61
- let inner = track(true);
59
+ let outer = #ripple.track(true);
60
+ let inner = #ripple.track(true);
62
61
  <button
63
62
  class="outer-toggle"
64
63
  onClick={() => {
@@ -86,7 +85,7 @@ export component NestedIf() {
86
85
  }
87
86
 
88
87
  export component IfElseIfChain() {
89
- let status = track<'loading' | 'success' | 'error'>('loading');
88
+ let status = #ripple.track<'loading' | 'success' | 'error'>('loading');
90
89
  <div>
91
90
  <button
92
91
  class="success"
@@ -1,5 +1,3 @@
1
- import { track } from 'ripple';
2
-
3
1
  export component MixedControlFlowStatic() {
4
2
  const rows = [
5
3
  { id: 1, kind: 'a', enabled: true },
@@ -31,9 +29,9 @@ export component MixedControlFlowStatic() {
31
29
  }
32
30
 
33
31
  export component MixedControlFlowReactive() {
34
- let show = track(true);
35
- let mode = track<'a' | 'b'>('a');
36
- let items = track([
32
+ let show = #ripple.track(true);
33
+ let mode = #ripple.track<'a' | 'b'>('a');
34
+ let items = #ripple.track([
37
35
  { id: 1, label: 'One' },
38
36
  { id: 2, label: 'Two' },
39
37
  ]);
@@ -108,7 +106,7 @@ export component MixedControlFlowAsyncPending() {
108
106
  }
109
107
  }
110
108
 
111
- component AsyncRow({ label }) {
109
+ component AsyncRow({ label }: { label: string }) {
112
110
  let value = await Promise.resolve(label);
113
111
  <div class="resolved-row">{value}</div>
114
112
  }
@@ -13,7 +13,7 @@ export component SimplePortal() {
13
13
 
14
14
  // Portal with conditional rendering
15
15
  export component ConditionalPortal() {
16
- let show = @true;
16
+ let show = #ripple.track(true);
17
17
 
18
18
  <div class="container">
19
19
  <button class="toggle" onClick={() => (@show = !@show)}>{'Toggle'}</button>
@@ -1,13 +1,12 @@
1
1
  // Reactive components for hydration testing
2
- import { track } from 'ripple';
3
2
 
4
3
  export component TrackedState() {
5
- let count = track(0);
4
+ let count = #ripple.track(0);
6
5
  <div class="count">{@count}</div>
7
6
  }
8
7
 
9
8
  export component CounterWithInitial(props: { initial: number }) {
10
- let count = track(props.initial);
9
+ let count = #ripple.track(props.initial);
11
10
  <div>
12
11
  <span class="count">{@count}</span>
13
12
  </div>
@@ -18,24 +17,24 @@ export component CounterWrapper() {
18
17
  }
19
18
 
20
19
  export component ComputedValues() {
21
- let a = track(2);
22
- let b = track(3);
20
+ let a = #ripple.track(2);
21
+ let b = #ripple.track(3);
23
22
  const sum = () => @a + @b;
24
23
  <div class="sum">{sum()}</div>
25
24
  }
26
25
 
27
26
  export component MultipleTracked() {
28
- let x = track(10);
29
- let y = track(20);
30
- let z = track(30);
27
+ let x = #ripple.track(10);
28
+ let y = #ripple.track(20);
29
+ let z = #ripple.track(30);
31
30
  <div class="x">{@x}</div>
32
31
  <div class="y">{@y}</div>
33
32
  <div class="z">{@z}</div>
34
33
  }
35
34
 
36
35
  export component DerivedState() {
37
- let firstName = track('John');
38
- let lastName = track('Doe');
36
+ let firstName = #ripple.track('John');
37
+ let lastName = #ripple.track('Doe');
39
38
  const fullName = () => `${@firstName} ${@lastName}`;
40
39
  <div class="name">{fullName()}</div>
41
40
  }
@@ -1,5 +1,4 @@
1
1
  // Return statement components for hydration testing
2
- import { track } from 'ripple';
3
2
 
4
3
  // Basic return - skips content after direct return
5
4
  export component DirectReturn() {
@@ -265,7 +264,7 @@ export component ReturnWithElseBothReturn() {
265
264
 
266
265
  // Reactive return - starts true, can toggle to false
267
266
  export component ReactiveReturnTrueToFalse() {
268
- let condition = track(true);
267
+ let condition = #ripple.track(true);
269
268
 
270
269
  <button
271
270
  class="toggle"
@@ -284,7 +283,7 @@ export component ReactiveReturnTrueToFalse() {
284
283
 
285
284
  // Reactive return - starts false, can toggle to true
286
285
  export component ReactiveReturnFalseToTrue() {
287
- let condition = track(false);
286
+ let condition = #ripple.track(false);
288
287
 
289
288
  <button
290
289
  class="toggle"
@@ -304,7 +303,7 @@ export component ReactiveReturnFalseToTrue() {
304
303
  // Reactive nested return - only inner condition (b) is tracked
305
304
  export component ReactiveNestedReturn() {
306
305
  let a = true;
307
- let b = track(true);
306
+ let b = #ripple.track(true);
308
307
 
309
308
  <button
310
309
  class="toggle"
@@ -397,7 +396,7 @@ export component MultipleSiblingReturns() {
397
396
 
398
397
  // Reactive sibling returns - cycles first -> second -> fallback
399
398
  export component ReactiveSiblingReturns() {
400
- let mode = track('first');
399
+ let mode = #ripple.track('first');
401
400
 
402
401
  <button
403
402
  class="toggle"
@@ -429,8 +428,8 @@ export component ReactiveSiblingReturns() {
429
428
 
430
429
  // Reactive nested returns with tracked outer and inner conditions
431
430
  export component ReactiveOuterInnerReturns() {
432
- let a = track(true);
433
- let b = track(true);
431
+ let a = #ripple.track(true);
432
+ let b = #ripple.track(true);
434
433
 
435
434
  <button
436
435
  class="toggle-a"
@@ -463,7 +462,7 @@ export component ReactiveOuterInnerReturns() {
463
462
 
464
463
  // Reactive else-if return chain that transitions between return and non-return states
465
464
  export component ReactiveElseIfReturns() {
466
- let status = track(0);
465
+ let status = #ripple.track(0);
467
466
 
468
467
  <button
469
468
  class="toggle"
@@ -488,10 +487,10 @@ export component ReactiveElseIfReturns() {
488
487
 
489
488
  // Deeply nested independent return guards with multiple root-level siblings
490
489
  export component ReactiveDeepNestedIndependentReturns() {
491
- let c1 = track(false);
492
- let c2 = track(false);
493
- let c3 = track(false);
494
- let c4 = track(false);
490
+ let c1 = #ripple.track(false);
491
+ let c2 = #ripple.track(false);
492
+ let c3 = #ripple.track(false);
493
+ let c4 = #ripple.track(false);
495
494
 
496
495
  <button
497
496
  class="toggle-c1"
@@ -1,7 +1,5 @@
1
- import { track } from 'ripple';
2
-
3
1
  export component SwitchStatic() {
4
- const status = 'success';
2
+ const status: string = 'success';
5
3
  switch (status) {
6
4
  case 'success':
7
5
  <div class="status-success">{'Success'}</div>
@@ -15,7 +13,7 @@ export component SwitchStatic() {
15
13
  }
16
14
 
17
15
  export component SwitchReactive() {
18
- let status = track<'a' | 'b' | 'c'>('a');
16
+ let status = #ripple.track<'a' | 'b' | 'c'>('a');
19
17
  <button
20
18
  class="toggle"
21
19
  onClick={() => {
@@ -39,7 +37,7 @@ export component SwitchReactive() {
39
37
  }
40
38
 
41
39
  export component SwitchFallthrough() {
42
- const val = 1;
40
+ const val: number = 1;
43
41
  switch (val) {
44
42
  case 1:
45
43
  case 2:
@@ -51,7 +49,7 @@ export component SwitchFallthrough() {
51
49
  }
52
50
 
53
51
  export component SwitchNumericLevels() {
54
- let level = track<1 | 2 | 3>(1);
52
+ let level = #ripple.track<1 | 2 | 3>(1);
55
53
  <button
56
54
  class="level-toggle"
57
55
  onClick={() => {
@@ -76,7 +74,7 @@ export component SwitchNumericLevels() {
76
74
  }
77
75
 
78
76
  export component SwitchBlockScoped() {
79
- let level = track<1 | 2 | 3>(1);
77
+ let level = #ripple.track<1 | 2 | 3>(1);
80
78
  <button
81
79
  class="block-toggle"
82
80
  onClick={() => {
@@ -104,7 +102,7 @@ export component SwitchBlockScoped() {
104
102
  }
105
103
 
106
104
  export component SwitchNoBreak() {
107
- let level = track<1 | 2 | 3>(1);
105
+ let level = #ripple.track<1 | 2 | 3>(1);
108
106
  <button
109
107
  class="nobreak-toggle"
110
108
  onClick={() => {
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync } from 'ripple';
3
+ import { hydrateComponent, container } from '../setup-hydration.js';
4
+ import { hmr } from '../../src/runtime/internal/client/hmr.js';
5
+ import { HMR } from '../../src/runtime/internal/client/constants.js';
6
+
7
+ // Import server-compiled components
8
+ import * as ServerComponents from './compiled/server/hmr.js';
9
+ // Import client-compiled components
10
+ import * as ClientComponents from './compiled/client/hmr.js';
11
+
12
+ describe('hydration > HMR re-render', () => {
13
+ it('re-renders layout component correctly after hydration (no zoom/displacement)', async () => {
14
+ // Hydrate the layout+content component
15
+ await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
16
+
17
+ // Verify initial state
18
+ expect(container.querySelector('.layout')).not.toBeNull();
19
+ expect(container.querySelector('.nav')?.textContent).toBe('Navigation');
20
+ expect(container.querySelector('.main')).not.toBeNull();
21
+ expect(container.querySelector('.content')).not.toBeNull();
22
+ expect(container.querySelector('.text')?.textContent).toBe('Hello world');
23
+
24
+ // Wrap the layout component with HMR (simulates what the compiler does in dev mode)
25
+ const layout_hmr = hmr(ClientComponents.Layout);
26
+
27
+ // Create an "updated" version of the component (simulates saving the file)
28
+ function UpdatedLayout(anchor, props, block) {
29
+ return ClientComponents.Layout(anchor, props, block);
30
+ }
31
+ const incoming = hmr(UpdatedLayout);
32
+
33
+ // Simulate calling wrapper() to establish the HMR instance
34
+ // (In practice the component is already mounted via hydrateComponent above,
35
+ // but we test the HMR update mechanism directly)
36
+ const update_fn = layout_hmr[HMR].update;
37
+
38
+ // The update should not throw
39
+ expect(() => {
40
+ update_fn(incoming);
41
+ }).not.toThrow();
42
+ });
43
+
44
+ it('layout component remains inside container after hydration', async () => {
45
+ await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
46
+
47
+ // The layout div must be inside the container, not displaced
48
+ const layout = container.querySelector('.layout');
49
+ expect(layout).not.toBeNull();
50
+ expect(container.contains(layout)).toBe(true);
51
+
52
+ // The main content must be inside the layout
53
+ const main = layout?.querySelector('.main');
54
+ expect(main).not.toBeNull();
55
+
56
+ // The text content must be present and correct
57
+ expect(container.querySelector('.text')?.textContent).toBe('Hello world');
58
+ });
59
+
60
+ it('hydrates layout with nested if block without corrupting branch state', async () => {
61
+ await hydrateComponent(ServerComponents.LayoutWithContent, ClientComponents.LayoutWithContent);
62
+
63
+ // After hydration, the conditional content must still be visible
64
+ expect(container.querySelector('.text')).not.toBeNull();
65
+ expect(container.querySelector('.text')?.textContent).toBe('Hello world');
66
+
67
+ // All structural elements must be present in the correct hierarchy
68
+ const layout = container.querySelector('.layout');
69
+ expect(layout).not.toBeNull();
70
+ expect(layout?.querySelector('.nav')).not.toBeNull();
71
+ expect(layout?.querySelector('.main')).not.toBeNull();
72
+ expect(layout?.querySelector('.content')).not.toBeNull();
73
+ });
74
+ });
@@ -1,4 +1,4 @@
1
- import { track, set, get } from 'ripple';
1
+ import { set, get } from 'ripple';
2
2
 
3
3
  describe('await in control flow', () => {
4
4
  it('all tests are commented out for now as await in control flow is not yet supported', () => {
@@ -8,7 +8,7 @@ describe('await in control flow', () => {
8
8
  // it('should handle await inside if statement', async () => {
9
9
  // component App() {
10
10
  // let condition = true;
11
- // let data = track('loading');
11
+ // let data = #ripple.track('loading');
12
12
 
13
13
  // if (condition) {
14
14
  // await new Promise((resolve) => setTimeout(() => {
@@ -1,5 +1,3 @@
1
- import { track } from 'ripple';
2
-
3
1
  describe('basic server > attribute rendering', () => {
4
2
  it('render static attributes', async () => {
5
3
  component Basic() {
@@ -19,7 +17,7 @@ describe('basic server > attribute rendering', () => {
19
17
 
20
18
  it('render dynamic class attribute', async () => {
21
19
  component Basic() {
22
- let active = track(false);
20
+ let active = #ripple.track(false);
23
21
 
24
22
  <div class={@active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
25
23
 
@@ -89,7 +87,7 @@ describe('basic server > attribute rendering', () => {
89
87
 
90
88
  it('render dynamic class object', async () => {
91
89
  component Basic() {
92
- let active = track(false);
90
+ let active = #ripple.track(false);
93
91
 
94
92
  <div class={{ active: @active, inactive: !@active }}>{'Dynamic Class'}</div>
95
93
 
@@ -116,7 +114,7 @@ describe('basic server > attribute rendering', () => {
116
114
  'applies scoped ripple class to multiple elements with dynamic class expressions',
117
115
  async () => {
118
116
  component Basic() {
119
- let selected = track(1);
117
+ let selected = #ripple.track(1);
120
118
 
121
119
  <div class={@selected === 0 ? 'selected' : ''}>{`div 1`}</div>
122
120
  <div class={@selected === 0 ? 'selected' : ''}>{`div 2`}</div>
@@ -147,7 +145,7 @@ describe('basic server > attribute rendering', () => {
147
145
 
148
146
  it('render dynamic id attribute', async () => {
149
147
  component Basic() {
150
- let count = track(0);
148
+ let count = #ripple.track(0);
151
149
 
152
150
  <div id={`item-${@count}`}>{'Dynamic ID'}</div>
153
151
  }
@@ -162,7 +160,7 @@ describe('basic server > attribute rendering', () => {
162
160
 
163
161
  it('render dynamic style attribute', async () => {
164
162
  component Basic() {
165
- let color = track('red');
163
+ let color = #ripple.track('red');
166
164
 
167
165
  <div style={`color: ${@color}; font-weight: bold;`}>{'Dynamic Style'}</div>
168
166
  }
@@ -178,7 +176,7 @@ describe('basic server > attribute rendering', () => {
178
176
 
179
177
  it('render style attribute as dynamic object', async () => {
180
178
  component Basic() {
181
- let color = track('red');
179
+ let color = #ripple.track('red');
182
180
 
183
181
  <div style={{ color: @color, fontWeight: 'bold' }}>{'Dynamic Style'}</div>
184
182
  }
@@ -194,7 +192,7 @@ describe('basic server > attribute rendering', () => {
194
192
 
195
193
  it('render tracked variable as style attribute', async () => {
196
194
  component Basic() {
197
- let style = track({ color: 'red', fontWeight: 'bold' });
195
+ let style = #ripple.track({ color: 'red', fontWeight: 'bold' });
198
196
 
199
197
  <div {@style}>{'Dynamic Style'}</div>
200
198
  }
@@ -210,7 +208,7 @@ describe('basic server > attribute rendering', () => {
210
208
 
211
209
  it('render tracked object as style attribute', async () => {
212
210
  component Basic() {
213
- let style = #{ color: 'red', fontWeight: 'bold' };
211
+ let style = #ripple{ color: 'red', fontWeight: 'bold' };
214
212
 
215
213
  <div style={{ color: style.color, fontWeight: style.fontWeight }}>{'Dynamic Style'}</div>
216
214
  }
@@ -277,8 +275,8 @@ describe('basic server > attribute rendering', () => {
277
275
 
278
276
  it('render dynamic boolean attributes as false', async () => {
279
277
  component Basic() {
280
- let disabled = track(false);
281
- let checked = track(false);
278
+ let disabled = #ripple.track(false);
279
+ let checked = #ripple.track(false);
282
280
 
283
281
  <input type="checkbox" {@disabled} {@checked} />
284
282
  }
@@ -312,8 +310,8 @@ describe('basic server > attribute rendering', () => {
312
310
 
313
311
  it('render dynamic boolean attributes as true', async () => {
314
312
  component Basic() {
315
- let disabled = track(true);
316
- let checked = track(true);
313
+ let disabled = #ripple.track(true);
314
+ let checked = #ripple.track(true);
317
315
 
318
316
  <input type="checkbox" {@disabled} {@checked} />
319
317
  }
@@ -347,8 +345,8 @@ describe('basic server > attribute rendering', () => {
347
345
 
348
346
  it('render multiple dynamic attributes', async () => {
349
347
  component Basic() {
350
- let theme = track('light');
351
- let size = track('medium');
348
+ let theme = #ripple.track('light');
349
+ let size = #ripple.track('medium');
352
350
 
353
351
  <div class={`theme-${@theme} size-${@size}`} data-theme={@theme} data-size={@size}>
354
352
  {'Multiple Dynamic Attributes'}
@@ -367,8 +365,8 @@ describe('basic server > attribute rendering', () => {
367
365
 
368
366
  it('render conditional attributes', async () => {
369
367
  component Basic() {
370
- let showTitle = track(false);
371
- let showAria = track(false);
368
+ let showTitle = #ripple.track(false);
369
+ let showAria = #ripple.track(false);
372
370
 
373
371
  <div
374
372
  title={@showTitle ? 'This is a title' : undefined}
@@ -389,7 +387,7 @@ describe('basic server > attribute rendering', () => {
389
387
 
390
388
  it('render spread attributes', async () => {
391
389
  component Basic() {
392
- let attrs = track<TestAttributes>(
390
+ let attrs = #ripple.track<TestAttributes>(
393
391
  {
394
392
  class: 'initial',
395
393
  id: 'test-1',
@@ -411,12 +409,12 @@ describe('basic server > attribute rendering', () => {
411
409
 
412
410
  it('renders with reactive attributes with nested reactive attributes', async () => {
413
411
  component Basic() {
414
- let value = track('parent-class');
412
+ let value = #ripple.track('parent-class');
415
413
 
416
414
  <p class={@value}>{'Colored parent value'}</p>
417
415
 
418
416
  <div>
419
- let nested = track('nested-class');
417
+ let nested = #ripple.track('nested-class');
420
418
 
421
419
  <p class={@nested}>{'Colored nested value'}</p>
422
420
  </div>
@@ -1,5 +1,10 @@
1
- import type { Tracked, PropsWithChildren, PropsWithExtras } from 'ripple';
2
- import { track } from 'ripple';
1
+ import type {
2
+ Tracked,
3
+ PropsWithChildren,
4
+ PropsWithExtras,
5
+ Component,
6
+ PropsWithChildrenOptional,
7
+ } from 'ripple';
3
8
 
4
9
  describe('basic server > components & composition', () => {
5
10
  it('renders with component composition and children', async () => {
@@ -28,8 +33,9 @@ describe('basic server > components & composition', () => {
28
33
  });
29
34
 
30
35
  it('does not render a falsy component call', async () => {
31
- component Card(props: PropsWithChildren<{}>) {
36
+ component Card(props: PropsWithChildrenOptional<{ test: Component }>) {
32
37
  <div class="card">
38
+ // @ts-expect-error - ripple automatically handles falsy children
33
39
  <props.children />
34
40
  </div>
35
41
  }
@@ -100,7 +106,7 @@ describe('basic server > components & composition', () => {
100
106
  }
101
107
 
102
108
  component Basic() {
103
- let clicked = track(false);
109
+ let clicked = #ripple.track(false);
104
110
 
105
111
  <Card
106
112
  title="Test Card"
@@ -137,8 +143,8 @@ describe('basic server > components & composition', () => {
137
143
  }
138
144
 
139
145
  component Basic() {
140
- let message = track('Hello');
141
- let number = track(1);
146
+ let message = #ripple.track('Hello');
147
+ let number = #ripple.track(1);
142
148
 
143
149
  <ChildComponent text={message} count={number} />
144
150
  <button
@@ -225,7 +231,7 @@ describe('basic server > components & composition', () => {
225
231
  }
226
232
 
227
233
  component App() {
228
- let Content = track(() => Noop);
234
+ let Content = #ripple.track(() => Noop);
229
235
  <@Content />
230
236
  }
231
237