ripple 0.3.72 → 0.3.76

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/CHANGELOG.md +116 -0
  2. package/package.json +3 -3
  3. package/src/jsx-runtime.d.ts +4 -10
  4. package/src/runtime/dynamic-client.js +33 -0
  5. package/src/runtime/dynamic-server.js +80 -0
  6. package/src/runtime/index-client.js +5 -13
  7. package/src/runtime/index-server.js +2 -0
  8. package/src/runtime/internal/client/blocks.js +6 -27
  9. package/src/runtime/internal/client/composite.js +11 -6
  10. package/src/runtime/internal/client/for.js +80 -5
  11. package/src/runtime/internal/client/index.js +0 -2
  12. package/src/runtime/internal/client/render.js +5 -2
  13. package/src/runtime/internal/client/types.d.ts +0 -10
  14. package/src/runtime/internal/server/index.js +8 -1
  15. package/tests/client/__snapshots__/computed-properties.test.tsrx.snap +8 -0
  16. package/tests/client/__snapshots__/for.test.tsrx.snap +22 -0
  17. package/tests/client/__snapshots__/html.test.tsrx.snap +4 -0
  18. package/tests/client/array/array.copy-within.test.tsrx +19 -19
  19. package/tests/client/array/array.derived.test.tsrx +97 -109
  20. package/tests/client/array/array.iteration.test.tsrx +28 -28
  21. package/tests/client/array/array.mutations.test.tsrx +68 -68
  22. package/tests/client/array/array.static.test.tsrx +82 -92
  23. package/tests/client/array/array.to-methods.test.tsrx +15 -15
  24. package/tests/client/async-suspend.test.tsrx +180 -179
  25. package/tests/client/basic/__snapshots__/basic.attributes.test.tsrx.snap +2 -0
  26. package/tests/client/basic/__snapshots__/basic.rendering.test.tsrx.snap +4 -0
  27. package/tests/client/basic/basic.attributes.test.tsrx +273 -317
  28. package/tests/client/basic/basic.collections.test.tsrx +55 -61
  29. package/tests/client/basic/basic.components.test.tsrx +198 -220
  30. package/tests/client/basic/basic.errors.test.tsrx +70 -76
  31. package/tests/client/basic/basic.events.test.tsrx +80 -85
  32. package/tests/client/basic/basic.get-set.test.tsrx +54 -64
  33. package/tests/client/basic/basic.hmr.test.tsrx +15 -19
  34. package/tests/client/basic/basic.reactivity.test.tsrx +121 -135
  35. package/tests/client/basic/basic.rendering.test.tsrx +273 -178
  36. package/tests/client/basic/basic.styling.test.tsrx +16 -14
  37. package/tests/client/basic/basic.utilities.test.tsrx +8 -10
  38. package/tests/client/boundaries.test.tsrx +18 -18
  39. package/tests/client/compiler/compiler.assignments.test.tsrx +77 -76
  40. package/tests/client/compiler/compiler.attributes.test.tsrx +18 -14
  41. package/tests/client/compiler/compiler.basic.test.tsrx +357 -288
  42. package/tests/client/compiler/compiler.regex.test.tsrx +40 -44
  43. package/tests/client/compiler/compiler.tracked-access.test.tsrx +57 -38
  44. package/tests/client/compiler/compiler.try-in-function.test.tsrx +16 -16
  45. package/tests/client/compiler/compiler.typescript.test.tsrx +4 -3
  46. package/tests/client/composite/composite.dynamic-components.test.tsrx +62 -47
  47. package/tests/client/composite/composite.generics.test.tsrx +165 -167
  48. package/tests/client/composite/composite.props.test.tsrx +66 -74
  49. package/tests/client/composite/composite.reactivity.test.tsrx +132 -166
  50. package/tests/client/composite/composite.render.test.tsrx +92 -101
  51. package/tests/client/computed-properties.test.tsrx +14 -18
  52. package/tests/client/context.test.tsrx +14 -18
  53. package/tests/client/css/global-additional-cases.test.tsrx +493 -439
  54. package/tests/client/css/global-advanced-selectors.test.tsrx +169 -153
  55. package/tests/client/css/global-at-rules.test.tsrx +71 -66
  56. package/tests/client/css/global-basic.test.tsrx +105 -98
  57. package/tests/client/css/global-classes-ids.test.tsrx +128 -114
  58. package/tests/client/css/global-combinators.test.tsrx +83 -78
  59. package/tests/client/css/global-complex-nesting.test.tsrx +134 -120
  60. package/tests/client/css/global-edge-cases.test.tsrx +138 -120
  61. package/tests/client/css/global-keyframes.test.tsrx +108 -96
  62. package/tests/client/css/global-nested.test.tsrx +88 -78
  63. package/tests/client/css/global-pseudo.test.tsrx +104 -98
  64. package/tests/client/css/global-scoping.test.tsrx +145 -125
  65. package/tests/client/css/style-identifier.test.tsrx +65 -72
  66. package/tests/client/date.test.tsrx +83 -83
  67. package/tests/client/dynamic-elements.test.tsrx +318 -299
  68. package/tests/client/events.test.tsrx +252 -266
  69. package/tests/client/for.test.tsrx +120 -127
  70. package/tests/client/head.test.tsrx +74 -48
  71. package/tests/client/html.test.tsrx +37 -49
  72. package/tests/client/input-value.test.tsrx +1125 -1354
  73. package/tests/client/lazy-array.test.tsrx +10 -16
  74. package/tests/client/lazy-destructuring.test.tsrx +169 -221
  75. package/tests/client/map.test.tsrx +39 -41
  76. package/tests/client/media-query.test.tsrx +15 -19
  77. package/tests/client/object.test.tsrx +46 -56
  78. package/tests/client/portal.test.tsrx +31 -37
  79. package/tests/client/ref.test.tsrx +173 -193
  80. package/tests/client/return.test.tsrx +62 -37
  81. package/tests/client/set.test.tsrx +33 -33
  82. package/tests/client/svg.test.tsrx +197 -216
  83. package/tests/client/switch.test.tsrx +201 -191
  84. package/tests/client/track-async-hydration.test.tsrx +14 -18
  85. package/tests/client/tracked-index-access.test.tsrx +18 -28
  86. package/tests/client/try.test.tsrx +494 -619
  87. package/tests/client/tsx.test.tsrx +286 -292
  88. package/tests/client/typescript-generics.test.tsrx +121 -129
  89. package/tests/client/url/url.derived.test.tsrx +21 -25
  90. package/tests/client/url/url.parsing.test.tsrx +35 -35
  91. package/tests/client/url/url.partial-removal.test.tsrx +32 -32
  92. package/tests/client/url/url.reactivity.test.tsrx +68 -72
  93. package/tests/client/url/url.serialization.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +21 -27
  95. package/tests/client/url-search-params/url-search-params.initialization.test.tsrx +16 -16
  96. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +37 -37
  97. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +56 -60
  98. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +32 -34
  99. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +9 -9
  100. package/tests/client/url-search-params/url-search-params.tracked-url.test.tsrx +10 -10
  101. package/tests/hydration/compiled/client/basic.js +390 -319
  102. package/tests/hydration/compiled/client/composite.js +52 -44
  103. package/tests/hydration/compiled/client/for.js +734 -604
  104. package/tests/hydration/compiled/client/head.js +183 -103
  105. package/tests/hydration/compiled/client/html.js +93 -86
  106. package/tests/hydration/compiled/client/if-children.js +95 -71
  107. package/tests/hydration/compiled/client/if.js +113 -89
  108. package/tests/hydration/compiled/client/mixed-control-flow.js +225 -209
  109. package/tests/hydration/compiled/client/nested-control-flow.js +94 -98
  110. package/tests/hydration/compiled/client/reactivity.js +26 -24
  111. package/tests/hydration/compiled/client/return.js +8 -42
  112. package/tests/hydration/compiled/client/switch.js +208 -173
  113. package/tests/hydration/compiled/client/track-async-serialization.js +176 -128
  114. package/tests/hydration/compiled/client/try.js +29 -21
  115. package/tests/hydration/compiled/server/basic.js +210 -221
  116. package/tests/hydration/compiled/server/composite.js +13 -14
  117. package/tests/hydration/compiled/server/for.js +427 -444
  118. package/tests/hydration/compiled/server/head.js +199 -189
  119. package/tests/hydration/compiled/server/html.js +33 -41
  120. package/tests/hydration/compiled/server/if-children.js +114 -117
  121. package/tests/hydration/compiled/server/if.js +77 -83
  122. package/tests/hydration/compiled/server/mixed-control-flow.js +145 -150
  123. package/tests/hydration/compiled/server/nested-control-flow.js +10 -0
  124. package/tests/hydration/compiled/server/reactivity.js +24 -22
  125. package/tests/hydration/compiled/server/return.js +6 -18
  126. package/tests/hydration/compiled/server/switch.js +179 -176
  127. package/tests/hydration/compiled/server/track-async-serialization.js +88 -70
  128. package/tests/hydration/compiled/server/try.js +31 -35
  129. package/tests/hydration/components/basic.tsrx +216 -258
  130. package/tests/hydration/components/composite.tsrx +32 -42
  131. package/tests/hydration/components/events.tsrx +81 -101
  132. package/tests/hydration/components/for.tsrx +270 -336
  133. package/tests/hydration/components/head.tsrx +43 -39
  134. package/tests/hydration/components/hmr.tsrx +16 -22
  135. package/tests/hydration/components/html-in-template.tsrx +15 -21
  136. package/tests/hydration/components/html.tsrx +442 -526
  137. package/tests/hydration/components/if-children.tsrx +107 -125
  138. package/tests/hydration/components/if.tsrx +68 -90
  139. package/tests/hydration/components/mixed-control-flow.tsrx +65 -72
  140. package/tests/hydration/components/nested-control-flow.tsrx +202 -216
  141. package/tests/hydration/components/portal.tsrx +33 -41
  142. package/tests/hydration/components/reactivity.tsrx +26 -34
  143. package/tests/hydration/components/return.tsrx +4 -6
  144. package/tests/hydration/components/switch.tsrx +73 -78
  145. package/tests/hydration/components/track-async-serialization.tsrx +83 -93
  146. package/tests/hydration/components/try.tsrx +37 -51
  147. package/tests/hydration/switch.test.js +8 -8
  148. package/tests/server/await.test.tsrx +3 -3
  149. package/tests/server/basic.attributes.test.tsrx +117 -162
  150. package/tests/server/basic.components.test.tsrx +164 -194
  151. package/tests/server/basic.test.tsrx +299 -199
  152. package/tests/server/compiler.test.tsrx +142 -72
  153. package/tests/server/composite.props.test.tsrx +54 -58
  154. package/tests/server/composite.test.tsrx +165 -167
  155. package/tests/server/context.test.tsrx +13 -17
  156. package/tests/server/dynamic-elements.test.tsrx +147 -148
  157. package/tests/server/for.test.tsrx +115 -84
  158. package/tests/server/head.test.tsrx +54 -31
  159. package/tests/server/html-nesting-validation.test.tsrx +16 -8
  160. package/tests/server/if.test.tsrx +49 -59
  161. package/tests/server/lazy-destructuring.test.tsrx +288 -366
  162. package/tests/server/return.test.tsrx +58 -36
  163. package/tests/server/streaming-ssr.test.tsrx +4 -4
  164. package/tests/server/style-identifier.test.tsrx +61 -69
  165. package/tests/server/switch.test.tsrx +89 -97
  166. package/tests/server/track-async-serialization.test.tsrx +85 -103
  167. package/tests/server/try.test.tsrx +275 -360
  168. package/tests/utils/ref-types.test.js +72 -0
  169. package/tests/utils/vite-plugin-config.test.js +41 -74
  170. package/types/index.d.ts +29 -4
  171. package/src/runtime/internal/client/compat.js +0 -40
  172. package/tests/utils/compiler-compat-config.test.js +0 -38
@@ -3,13 +3,11 @@ import { compile } from '@tsrx/ripple';
3
3
 
4
4
  describe('for statements', () => {
5
5
  it('renders a simple static array', () => {
6
- function App() {
7
- return <>
8
- const items = ['Item 1', 'Item 2', 'Item 3'];
9
- for (const item of items) {
10
- <div class={item}>{item}</div>
11
- }
12
- </>;
6
+ function App() @{
7
+ const items = ['Item 1', 'Item 2', 'Item 3'];
8
+ @for (const item of items) {
9
+ <div class={item}>{item}</div>
10
+ }
13
11
  }
14
12
 
15
13
  render(App);
@@ -17,15 +15,28 @@ describe('for statements', () => {
17
15
  expect(container).toMatchSnapshot();
18
16
  });
19
17
 
20
- it('allows continue to skip an iteration', () => {
21
- function App() {
22
- return <>
23
- const items = ['Item 1', '', 'Item 3'];
24
- for (const item of items) {
25
- if (!item) continue;
26
- <div class="item">{item}</div>
27
- }
28
- </>;
18
+ it('allows plain JavaScript control flow in setup when rendered output follows', () => {
19
+ expect(
20
+ () => compile(
21
+ `function App() @{
22
+ const items = [1, 2, 3];
23
+ for (const item of items) {
24
+ <div class="selected">{item}</div>
25
+ }
26
+ <></>
27
+ }`,
28
+ 'App.tsrx',
29
+ { mode: 'client' },
30
+ ),
31
+ ).not.toThrow();
32
+ });
33
+
34
+ it('renders filtered items before passing them to @for', () => {
35
+ function App() @{
36
+ const items = ['Item 1', '', 'Item 3'].filter(Boolean);
37
+ @for (const item of items) {
38
+ <div class="item">{item}</div>
39
+ }
29
40
  }
30
41
 
31
42
  render(App);
@@ -36,63 +47,47 @@ describe('for statements', () => {
36
47
  ]);
37
48
  });
38
49
 
39
- it('allows continue after setup statements to skip an iteration', () => {
40
- const skipped = [];
41
-
42
- function App() {
43
- return <>
44
- const items = ['Item 1', '', 'Item 3'];
45
- for (const item of items) {
46
- if (!item) {
47
- skipped.push('skip');
48
- continue;
49
- }
50
- <div class="item">{item}</div>
51
- }
52
- </>;
50
+ it('renders an empty fallback', () => {
51
+ function App() @{
52
+ const items = [];
53
+ @for (const item of items) {
54
+ <div class="item">{item}</div>
55
+ } @empty {
56
+ <div class="empty">{'No items'}</div>
57
+ }
53
58
  }
54
59
 
55
60
  render(App);
56
61
 
57
- expect(skipped).toEqual(['skip']);
58
- expect(Array.from(container.querySelectorAll('.item')).map((el) => el.textContent)).toEqual([
59
- 'Item 1',
60
- 'Item 3',
61
- ]);
62
+ expect(container.querySelector('.empty').textContent).toBe('No items');
63
+ expect(container.querySelector('.item')).toBeNull();
62
64
  });
63
65
 
64
- it('does not emit JavaScript continue in for...of skip callbacks', () => {
65
- const { code } = compile(
66
- `function App() { return <>
66
+ it('throws for continue statements inside for...of loops', () => {
67
+ expect(
68
+ () => compile(
69
+ `function App() @{
67
70
  const items = ['Item 1', '', 'Item 3'];
68
- const skipped = [];
69
-
70
- for (const item of items) {
71
- if (!item) {
72
- skipped.push('skip');
73
- continue;
74
- }
71
+ @for (const item of items) {
72
+ if (!item) continue;
75
73
  <div class="item">{item}</div>
76
74
  }
77
- </>; }`,
78
- 'App.tsrx',
79
- { mode: 'client' },
80
- );
81
-
82
- expect(code).toContain('skipped.push(\'skip\')');
83
- expect(code).not.toContain('continue;');
84
- expect(code).not.toMatch(/continue;\s*return/);
75
+ }`,
76
+ 'App.tsrx',
77
+ { mode: 'client' },
78
+ ),
79
+ ).toThrow('Continue statements are not allowed inside TSRX template for...of loops');
85
80
  });
86
81
 
87
82
  it('renders a simple dynamic array', () => {
88
- function App() {
89
- return <>
90
- const items = new RippleArray('Item 1', 'Item 2', 'Item 3');
91
- for (const item of items) {
83
+ function App() @{
84
+ const items = new RippleArray('Item 1', 'Item 2', 'Item 3');
85
+ <>
86
+ @for (const item of items) {
92
87
  <div class={item}>{item}</div>
93
88
  }
94
89
  <button onClick={() => items.push(`Item ${items.length + 1}`)}>{'Add Item'}</button>
95
- </>;
90
+ </>
96
91
  }
97
92
 
98
93
  render(App);
@@ -107,20 +102,20 @@ describe('for statements', () => {
107
102
  });
108
103
 
109
104
  it('correctly handles intermediate statements in for block', () => {
110
- function App() {
111
- return <>
112
- const items = new RippleArray(1, 2, 3);
105
+ function App() @{
106
+ const items = new RippleArray(1, 2, 3);
107
+ <>
113
108
  <div>
114
- for (const item of items) {
109
+ @for (const item of items) {
110
+ const some_text = item;
115
111
  <div>
116
112
  <div>{item}</div>
117
- const some_text = item;
118
113
  <div>{some_text}</div>
119
114
  </div>
120
115
  }
121
116
  </div>
122
117
  <button onClick={() => items.push(items.length + 1)}>{'Add Item'}</button>
123
- </>;
118
+ </>
124
119
  }
125
120
 
126
121
  render(App);
@@ -136,19 +131,21 @@ describe('for statements', () => {
136
131
  });
137
132
 
138
133
  it('correctly handles the index in a for...of loop', () => {
139
- function App() {
140
- return <>
141
- const items = new RippleArray('a', 'b', 'c');
134
+ function App() @{
135
+ const items = new RippleArray('a', 'b', 'c');
136
+ <>
142
137
  <div>
143
- for (let item of items; index i) {
144
- <div>{i + ' : ' + item}</div>
138
+ @for (let item of items; index i) {
139
+ <div>
140
+ {i + ' : ' + item}
141
+ </div>
145
142
  }
146
143
  </div>
147
- <button onClick={() => items.push(String.fromCharCode(97 + items.length))}>
148
- {'Add Item'}
149
- </button>
144
+ <button
145
+ onClick={() => items.push(String.fromCharCode(97 + items.length))}
146
+ >{'Add Item'}</button>
150
147
  <button onClick={() => items.reverse()}>{'Reverse'}</button>
151
- </>;
148
+ </>
152
149
  }
153
150
 
154
151
  render(App);
@@ -169,24 +166,24 @@ describe('for statements', () => {
169
166
  });
170
167
 
171
168
  it('correctly handles keyed for...of loops', () => {
172
- function App() {
173
- return <>
174
- let &[items] = track([
175
- { id: 1, text: 'Item 1' },
176
- { id: 2, text: 'Item 2' },
177
- { id: 3, text: 'Item 3' },
178
- ]);
179
- for (let item of items; index i; key item.id) {
180
- <div>{i + ':' + item.text}</div>
169
+ function App() @{
170
+ let &[items] = track([
171
+ { id: 1, text: 'Item 1' },
172
+ { id: 2, text: 'Item 2' },
173
+ { id: 3, text: 'Item 3' },
174
+ ]);
175
+ <>
176
+ @for (let item of items; index i; key item.id) {
177
+ <div>
178
+ {i + ':' + item.text}
179
+ </div>
181
180
  }
182
181
  <button
183
182
  onClick={() => {
184
183
  items = items.toReversed();
185
184
  }}
186
- >
187
- {'Reverse'}
188
- </button>
189
- </>;
185
+ >{'Reverse'}</button>
186
+ </>
190
187
  }
191
188
 
192
189
  render(App);
@@ -202,24 +199,22 @@ describe('for statements', () => {
202
199
  });
203
200
 
204
201
  it('keyed for over derived updates sibling text nodes', () => {
205
- function App() {
206
- return <>
207
- let &[count] = track(0);
208
- let &[items] = track(
209
- () => Array.from({ length: count }).map((_, id) => ({ id, label: `Item ${id}` })),
210
- );
202
+ function App() @{
203
+ let &[count] = track(0);
204
+ let &[items] = track(
205
+ () => Array.from({ length: count }).map((_, id) => ({ id, label: `Item ${id}` })),
206
+ );
207
+ <>
211
208
  <button
212
209
  onClick={() => {
213
210
  count++;
214
211
  }}
215
- >
216
- {'Add'}
217
- </button>
218
- for (const item of items; key item.id) {
212
+ >{'Add'}</button>
213
+ @for (const item of items; key item.id) {
219
214
  <div class="item">{item.label}</div>
220
215
  }
221
216
  <p class="count">{count}</p>
222
- </>;
217
+ </>
223
218
  }
224
219
 
225
220
  render(App);
@@ -243,22 +238,22 @@ describe('for statements', () => {
243
238
  });
244
239
 
245
240
  it('keyed for with 32+ items: full reversal updates values via Map path', () => {
246
- function App() {
247
- return <>
248
- let &[items] = track(Array.from({ length: 40 }, (_, i) => ({ id: i, text: `Item ${i}` })));
241
+ function App() @{
242
+ let &[items] = track(Array.from({ length: 40 }, (_, i) => ({ id: i, text: `Item ${i}` })));
243
+ <>
249
244
  <div>
250
- for (let item of items; index idx; key item.id) {
251
- <span class="item">{idx + ':' + item.text}</span>
245
+ @for (let item of items; index idx; key item.id) {
246
+ <span class="item">
247
+ {idx + ':' + item.text}
248
+ </span>
252
249
  }
253
250
  </div>
254
251
  <button
255
252
  onClick={() => {
256
253
  items = items.toReversed();
257
254
  }}
258
- >
259
- {'Reverse'}
260
- </button>
261
- </>;
255
+ >{'Reverse'}</button>
256
+ </>
262
257
  }
263
258
 
264
259
  render(App);
@@ -276,15 +271,17 @@ describe('for statements', () => {
276
271
  });
277
272
 
278
273
  it('handles updating with new objects with same key', () => {
279
- function App() {
280
- return <>
281
- let &[items] = track([
282
- { id: 1, text: 'Item 1' },
283
- { id: 2, text: 'Item 2' },
284
- { id: 3, text: 'Item 3' },
285
- ]);
286
- for (let item of items; index i; key item.id) {
287
- <div>{i + ':' + item.text}</div>
274
+ function App() @{
275
+ let &[items] = track([
276
+ { id: 1, text: 'Item 1' },
277
+ { id: 2, text: 'Item 2' },
278
+ { id: 3, text: 'Item 3' },
279
+ ]);
280
+ <>
281
+ @for (let item of items; index i; key item.id) {
282
+ <div>
283
+ {i + ':' + item.text}
284
+ </div>
288
285
  }
289
286
  <button
290
287
  onClick={() => {
@@ -298,10 +295,8 @@ describe('for statements', () => {
298
295
  { ...items[2], text: 'Item 3!' },
299
296
  ];
300
297
  }}
301
- >
302
- {'Reverse'}
303
- </button>
304
- </>;
298
+ >{'Reverse'}</button>
299
+ </>
305
300
  }
306
301
 
307
302
  render(App);
@@ -317,11 +312,11 @@ describe('for statements', () => {
317
312
  it('ref-based for with 32+ items: remove from start with shared refs via Map path', () => {
318
313
  const objects = Array.from({ length: 50 }, (_, i) => ({ id: i, text: `Obj ${i}` }));
319
314
 
320
- function App() {
321
- return <>
322
- let &[items] = track(objects.slice());
315
+ function App() @{
316
+ let &[items] = track(objects.slice());
317
+ <>
323
318
  <div>
324
- for (const item of items) {
319
+ @for (const item of items) {
325
320
  <span class="item">{item.text}</span>
326
321
  }
327
322
  </div>
@@ -329,10 +324,8 @@ describe('for statements', () => {
329
324
  onClick={() => {
330
325
  items = objects.slice(15).reverse();
331
326
  }}
332
- >
333
- {'Trim and reverse'}
334
- </button>
335
- </>;
327
+ >{'Trim and reverse'}</button>
328
+ </>
336
329
  }
337
330
 
338
331
  render(App);
@@ -14,13 +14,13 @@ describe('head elements', () => {
14
14
  });
15
15
 
16
16
  it('renders static title element', () => {
17
- function App() {
18
- return <>
17
+ function App() @{
18
+ <>
19
19
  <head>
20
20
  <title>{'Static Test Title'}</title>
21
21
  </head>
22
22
  <div>{'Content'}</div>
23
- </>;
23
+ </>
24
24
  }
25
25
 
26
26
  render(App);
@@ -30,9 +30,9 @@ describe('head elements', () => {
30
30
  });
31
31
 
32
32
  it('renders reactive title element', () => {
33
- function App() {
34
- return <>
35
- let &[title] = track('Initial Title');
33
+ function App() @{
34
+ let &[title] = track('Initial Title');
35
+ <>
36
36
  <head>
37
37
  <title>{title}</title>
38
38
  </head>
@@ -41,12 +41,10 @@ describe('head elements', () => {
41
41
  onClick={() => {
42
42
  title = 'Updated Title';
43
43
  }}
44
- >
45
- {'Update Title'}
46
- </button>
44
+ >{'Update Title'}</button>
47
45
  <span>{title}</span>
48
46
  </div>
49
- </>;
47
+ </>
50
48
  }
51
49
 
52
50
  render(App);
@@ -63,9 +61,9 @@ describe('head elements', () => {
63
61
  });
64
62
 
65
63
  it('renders title with template literal', () => {
66
- function App() {
67
- return <>
68
- let &[name] = track('World');
64
+ function App() @{
65
+ let &[name] = track('World');
66
+ <>
69
67
  <head>
70
68
  <title>{`Hello ${name}!`}</title>
71
69
  </head>
@@ -74,11 +72,9 @@ describe('head elements', () => {
74
72
  onClick={() => {
75
73
  name = 'Ripple';
76
74
  }}
77
- >
78
- {'Change Name'}
79
- </button>
75
+ >{'Change Name'}</button>
80
76
  </div>
81
- </>;
77
+ </>
82
78
  }
83
79
 
84
80
  render(App);
@@ -93,24 +89,24 @@ describe('head elements', () => {
93
89
  });
94
90
 
95
91
  it('renders title with computed value', () => {
96
- function App() {
97
- return <>
98
- let &[count] = track(0);
99
- let prefix = 'Count: ';
92
+ function App() @{
93
+ let &[count] = track(0);
94
+ let prefix = 'Count: ';
95
+ <>
100
96
  <head>
101
- <title>{prefix + count}</title>
97
+ <title>
98
+ {prefix + count}
99
+ </title>
102
100
  </head>
103
101
  <div>
104
102
  <button
105
103
  onClick={() => {
106
104
  count++;
107
105
  }}
108
- >
109
- {'Increment'}
110
- </button>
106
+ >{'Increment'}</button>
111
107
  <span>{count}</span>
112
108
  </div>
113
- </>;
109
+ </>
114
110
  }
115
111
 
116
112
  render(App);
@@ -126,9 +122,9 @@ describe('head elements', () => {
126
122
  });
127
123
 
128
124
  it('handles multiple title updates', () => {
129
- function App() {
130
- return <>
131
- let &[step] = track(1);
125
+ function App() @{
126
+ let &[step] = track(1);
127
+ <>
132
128
  <head>
133
129
  <title>{`Step ${step} of 3`}</title>
134
130
  </head>
@@ -137,11 +133,9 @@ describe('head elements', () => {
137
133
  onClick={() => {
138
134
  step = step % 3 + 1;
139
135
  }}
140
- >
141
- {'Next Step'}
142
- </button>
136
+ >{'Next Step'}</button>
143
137
  </div>
144
- </>;
138
+ </>
145
139
  }
146
140
 
147
141
  render(App);
@@ -164,13 +158,13 @@ describe('head elements', () => {
164
158
  });
165
159
 
166
160
  it('renders empty title', () => {
167
- function App() {
168
- return <>
161
+ function App() @{
162
+ <>
169
163
  <head>
170
164
  <title>{''}</title>
171
165
  </head>
172
166
  <div>{'Empty title test'}</div>
173
- </>;
167
+ </>
174
168
  }
175
169
 
176
170
  render(App);
@@ -178,31 +172,63 @@ describe('head elements', () => {
178
172
  expect(document.title).toBe('');
179
173
  });
180
174
 
175
+ it('renders external scripts with src attributes from a loop', () => {
176
+ const added: HTMLScriptElement[] = [];
177
+ const scripts = [{ src: '/a.js' }, { src: '/b.js' }];
178
+
179
+ function App() @{
180
+ <head>
181
+ @for (const script of scripts) {
182
+ <script src={script.src} />
183
+ }
184
+ </head>
185
+ }
186
+
187
+ try {
188
+ render(App);
189
+
190
+ const head_scripts = Array.from(
191
+ document.head.querySelectorAll('script[src]'),
192
+ ) as HTMLScriptElement[];
193
+
194
+ for (const node of head_scripts) {
195
+ if (node.getAttribute('src') === '/a.js' || node.getAttribute('src') === '/b.js') {
196
+ added.push(node);
197
+ }
198
+ }
199
+
200
+ const srcs = added.map((node) => node.getAttribute('src')).sort();
201
+ expect(srcs).toEqual(['/a.js', '/b.js']);
202
+ } finally {
203
+ for (const node of added) {
204
+ node.remove();
205
+ }
206
+ }
207
+ });
208
+
181
209
  it('renders title with conditional content', () => {
182
- function App() {
183
- return <>
184
- let &[showPrefix] = track(true);
185
- let &[title] = track('Main Page');
210
+ function App() @{
211
+ let &[showPrefix] = track(true);
212
+ let &[title] = track('Main Page');
213
+ <>
186
214
  <head>
187
- <title>{showPrefix ? 'App - ' + title : title}</title>
215
+ <title>
216
+ {showPrefix ? 'App - ' + title : title}
217
+ </title>
188
218
  </head>
189
219
  <div>
190
220
  <button
191
221
  onClick={() => {
192
222
  showPrefix = !showPrefix;
193
223
  }}
194
- >
195
- {'Toggle Prefix'}
196
- </button>
224
+ >{'Toggle Prefix'}</button>
197
225
  <button
198
226
  onClick={() => {
199
227
  title = title === 'Main Page' ? 'Settings' : 'Main Page';
200
228
  }}
201
- >
202
- {'Change Page'}
203
- </button>
229
+ >{'Change Page'}</button>
204
230
  </div>
205
- </>;
231
+ </>
206
232
  }
207
233
 
208
234
  render(App);