ripple 0.3.83 → 0.3.85

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 (58) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +4 -0
  4. package/src/runtime/index-client.js +2 -0
  5. package/src/runtime/index-server.js +13 -0
  6. package/src/runtime/internal/client/component.js +3 -3
  7. package/src/runtime/internal/client/constants.js +4 -0
  8. package/src/runtime/internal/client/for.js +22 -1
  9. package/src/runtime/internal/client/if.js +18 -2
  10. package/src/runtime/internal/client/index.js +1 -0
  11. package/src/runtime/internal/client/operations.js +25 -0
  12. package/src/runtime/internal/client/runtime.js +62 -10
  13. package/src/runtime/internal/client/switch.js +16 -2
  14. package/src/runtime/internal/client/template.js +10 -2
  15. package/src/runtime/internal/client/try.js +12 -2
  16. package/src/runtime/internal/client/types.d.ts +4 -0
  17. package/src/runtime/proxy.js +17 -1
  18. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -2
  19. package/tests/client/compiler/compiler.basic.test.tsrx +5 -2
  20. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -4
  21. package/tests/client/object.test.tsrx +47 -1
  22. package/tests/client/scoped-flush.test.tsrx +105 -0
  23. package/tests/hydration/compiled/client/basic.js +198 -214
  24. package/tests/hydration/compiled/client/composite.js +23 -72
  25. package/tests/hydration/compiled/client/for.js +66 -72
  26. package/tests/hydration/compiled/client/hmr.js +2 -13
  27. package/tests/hydration/compiled/client/html.js +619 -479
  28. package/tests/hydration/compiled/client/if-children.js +72 -84
  29. package/tests/hydration/compiled/client/if.js +87 -92
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +120 -160
  31. package/tests/hydration/compiled/client/nested-control-flow.js +288 -360
  32. package/tests/hydration/compiled/client/portal.js +15 -21
  33. package/tests/hydration/compiled/client/reactivity.js +7 -12
  34. package/tests/hydration/compiled/client/switch.js +113 -115
  35. package/tests/hydration/compiled/client/track-async-serialization.js +71 -122
  36. package/tests/hydration/compiled/client/try.js +26 -41
  37. package/tests/hydration/compiled/server/basic.js +268 -556
  38. package/tests/hydration/compiled/server/composite.js +31 -50
  39. package/tests/hydration/compiled/server/events.js +37 -173
  40. package/tests/hydration/compiled/server/for.js +236 -847
  41. package/tests/hydration/compiled/server/head.js +110 -241
  42. package/tests/hydration/compiled/server/hmr.js +14 -41
  43. package/tests/hydration/compiled/server/html-in-template.js +14 -38
  44. package/tests/hydration/compiled/server/html.js +951 -1176
  45. package/tests/hydration/compiled/server/if-children.js +59 -493
  46. package/tests/hydration/compiled/server/if.js +57 -209
  47. package/tests/hydration/compiled/server/mixed-control-flow.js +144 -250
  48. package/tests/hydration/compiled/server/nested-control-flow.js +309 -559
  49. package/tests/hydration/compiled/server/portal.js +94 -156
  50. package/tests/hydration/compiled/server/reactivity.js +23 -65
  51. package/tests/hydration/compiled/server/return.js +6 -16
  52. package/tests/hydration/compiled/server/switch.js +91 -219
  53. package/tests/hydration/compiled/server/track-async-serialization.js +211 -256
  54. package/tests/hydration/compiled/server/try.js +67 -126
  55. package/tests/hydration/components/html.tsrx +75 -0
  56. package/tests/hydration/html.test.js +50 -0
  57. package/tests/server/compiler.test.tsrx +6 -2
  58. package/types/index.d.ts +2 -0
@@ -6,15 +6,10 @@ import { trackAsync } from 'ripple/server';
6
6
  export function RootPending() {
7
7
  return _$_.tsrx_element(() => {
8
8
  _$_.regular_block(() => {
9
- _$_.output_push('<p');
10
- _$_.output_push(' class="root-pending"');
11
- _$_.output_push('>');
9
+ let __out = '';
12
10
 
13
- {
14
- _$_.output_push('root loading...');
15
- }
16
-
17
- _$_.output_push('</p>');
11
+ __out += '<p class="root-pending">root loading...</p>';
12
+ _$_.output_push(__out);
18
13
  });
19
14
  });
20
15
  }
@@ -22,32 +17,10 @@ export function RootPending() {
22
17
  export function RootCatch({ error, reset }) {
23
18
  return _$_.tsrx_element(() => {
24
19
  _$_.regular_block(() => {
25
- _$_.output_push('<section');
26
- _$_.output_push(' class="root-catch"');
27
- _$_.output_push('>');
28
-
29
- {
30
- _$_.output_push('<p');
31
- _$_.output_push(' class="root-error"');
32
- _$_.output_push('>');
33
-
34
- {
35
- _$_.output_push(_$_.escape(error.message));
36
- }
37
-
38
- _$_.output_push('</p>');
39
- _$_.output_push('<button');
40
- _$_.output_push(' class="root-reset"');
41
- _$_.output_push('>');
42
-
43
- {
44
- _$_.output_push('retry');
45
- }
46
-
47
- _$_.output_push('</button>');
48
- }
20
+ let __out = '';
49
21
 
50
- _$_.output_push('</section>');
22
+ __out += '<section class="root-catch"><p class="root-error">' + _$_.escape(error.message) + '</p><button class="root-reset">retry</button></section>';
23
+ _$_.output_push(__out);
51
24
  });
52
25
  });
53
26
  }
@@ -57,14 +30,10 @@ export function RootThrows() {
57
30
  throw new Error('root exploded');
58
31
 
59
32
  _$_.regular_block(() => {
60
- _$_.output_push('<p');
61
- _$_.output_push('>');
33
+ let __out = '';
62
34
 
63
- {
64
- _$_.output_push('should not render');
65
- }
66
-
67
- _$_.output_push('</p>');
35
+ __out += '<p>should not render</p>';
36
+ _$_.output_push(__out);
68
37
  });
69
38
  });
70
39
  }
@@ -74,15 +43,10 @@ export function RootAsyncDirect() {
74
43
  let lazy = _$_.track_async(() => Promise.resolve('root async value'), 'd6bf9e33');
75
44
 
76
45
  _$_.regular_block(() => {
77
- _$_.output_push('<p');
78
- _$_.output_push(' class="root-async-value"');
79
- _$_.output_push('>');
46
+ let __out = '';
80
47
 
81
- {
82
- _$_.output_push(_$_.escape(lazy.value));
83
- }
84
-
85
- _$_.output_push('</p>');
48
+ __out += '<p class="root-async-value">' + _$_.escape(lazy.value) + '</p>';
49
+ _$_.output_push(__out);
86
50
  });
87
51
  });
88
52
  }
@@ -92,15 +56,10 @@ export function RootAsyncRejects() {
92
56
  let lazy_1 = _$_.track_async(() => Promise.reject(new Error('root async failed')), 'd2fe7b64');
93
57
 
94
58
  _$_.regular_block(() => {
95
- _$_.output_push('<p');
96
- _$_.output_push(' class="root-async-value"');
97
- _$_.output_push('>');
59
+ let __out = '';
98
60
 
99
- {
100
- _$_.output_push(_$_.escape(lazy_1.value));
101
- }
102
-
103
- _$_.output_push('</p>');
61
+ __out += '<p class="root-async-value">' + _$_.escape(lazy_1.value) + '</p>';
62
+ _$_.output_push(__out);
104
63
  });
105
64
  });
106
65
  }
@@ -109,7 +68,11 @@ export function AsyncListInTryPending() {
109
68
  return _$_.tsrx_element(() => {
110
69
  _$_.try_block(
111
70
  () => {
112
- _$_.output_push('<!--[-->');
71
+ let __out = '';
72
+
73
+ __out += '<!--[-->';
74
+ _$_.output_push(__out);
75
+ __out = '';
113
76
 
114
77
  _$_.regular_block(() => {
115
78
  {
@@ -120,25 +83,26 @@ export function AsyncListInTryPending() {
120
83
  }
121
84
  });
122
85
 
123
- _$_.output_push('<!--]-->');
86
+ __out += '<!--]-->';
87
+ _$_.output_push(__out);
124
88
  },
125
89
  null,
126
90
  () => {
127
- _$_.output_push('<!--[-->');
91
+ let __out = '';
128
92
 
129
- _$_.regular_block(() => {
130
- _$_.output_push('<p');
131
- _$_.output_push(' class="loading"');
132
- _$_.output_push('>');
93
+ __out += '<!--[-->';
94
+ _$_.output_push(__out);
95
+ __out = '';
133
96
 
134
- {
135
- _$_.output_push('loading...');
136
- }
97
+ _$_.regular_block(() => {
98
+ let __out = '';
137
99
 
138
- _$_.output_push('</p>');
100
+ __out += '<p class="loading">loading...</p>';
101
+ _$_.output_push(__out);
139
102
  });
140
103
 
141
- _$_.output_push('<!--]-->');
104
+ __out += '<!--]-->';
105
+ _$_.output_push(__out);
142
106
  }
143
107
  );
144
108
  });
@@ -149,28 +113,16 @@ function AsyncList() {
149
113
  let lazy_2 = _$_.track_async(() => Promise.resolve(['alpha', 'beta', 'gamma']), 'b3d31627');
150
114
 
151
115
  _$_.regular_block(() => {
152
- _$_.output_push('<ul');
153
- _$_.output_push(' class="items"');
154
- _$_.output_push('>');
155
-
156
- {
157
- _$_.output_push('<!--[-->');
116
+ let __out = '';
158
117
 
159
- for (let item of lazy_2.value) {
160
- _$_.output_push('<li');
161
- _$_.output_push('>');
118
+ __out += '<ul class="items"><!--[-->';
162
119
 
163
- {
164
- _$_.output_push(_$_.escape(item));
165
- }
166
-
167
- _$_.output_push('</li>');
168
- }
169
-
170
- _$_.output_push('<!--]-->');
120
+ for (let item of lazy_2.value) {
121
+ __out += '<li>' + _$_.escape(item) + '</li>';
171
122
  }
172
123
 
173
- _$_.output_push('</ul>');
124
+ __out += '<!--]--></ul>';
125
+ _$_.output_push(__out);
174
126
  });
175
127
  });
176
128
  }
@@ -178,46 +130,40 @@ function AsyncList() {
178
130
  export function AsyncTryWithLeadingSibling() {
179
131
  return _$_.tsrx_element(() => {
180
132
  _$_.regular_block(() => {
181
- {
182
- _$_.output_push('<div');
183
- _$_.output_push(' class="before"');
184
- _$_.output_push('>');
133
+ let __out = '';
185
134
 
186
- {
187
- _$_.output_push('before');
188
- }
135
+ __out += '<div class="before">before</div>';
136
+ _$_.output_push(__out);
137
+ __out = '';
189
138
 
190
- _$_.output_push('</div>');
139
+ _$_.try_block(
140
+ () => {
141
+ let __out = '';
191
142
 
192
- _$_.try_block(
193
- () => {
194
- _$_.output_push('<!--[-->');
143
+ __out += '<!--[-->';
195
144
 
196
- {
197
- const comp = AsyncContent;
198
- const args = [{}];
145
+ {
146
+ const comp = AsyncContent;
147
+ const args = [{}];
199
148
 
200
- _$_.render_component(comp, ...args);
201
- }
149
+ _$_.output_push(__out);
150
+ __out = '';
151
+ _$_.render_component(comp, ...args);
152
+ }
202
153
 
203
- _$_.output_push('<!--]-->');
204
- },
205
- null,
206
- () => {
207
- _$_.output_push('<!--[-->');
208
- _$_.output_push('<div');
209
- _$_.output_push(' class="loading"');
210
- _$_.output_push('>');
154
+ __out += '<!--]-->';
155
+ _$_.output_push(__out);
156
+ },
157
+ null,
158
+ () => {
159
+ let __out = '';
211
160
 
212
- {
213
- _$_.output_push('loading async content');
214
- }
161
+ __out += '<!--[--><div class="loading">loading async content</div><!--]-->';
162
+ _$_.output_push(__out);
163
+ }
164
+ );
215
165
 
216
- _$_.output_push('</div>');
217
- _$_.output_push('<!--]-->');
218
- }
219
- );
220
- }
166
+ _$_.output_push(__out);
221
167
  });
222
168
  });
223
169
  }
@@ -227,15 +173,10 @@ function AsyncContent() {
227
173
  let lazy_3 = _$_.track_async(() => Promise.resolve('ready'), '15ea8758');
228
174
 
229
175
  _$_.regular_block(() => {
230
- _$_.output_push('<div');
231
- _$_.output_push(' class="resolved"');
232
- _$_.output_push('>');
233
-
234
- {
235
- _$_.output_push(_$_.escape(lazy_3.value));
236
- }
176
+ let __out = '';
237
177
 
238
- _$_.output_push('</div>');
178
+ __out += '<div class="resolved">' + _$_.escape(lazy_3.value) + '</div>';
179
+ _$_.output_push(__out);
239
180
  });
240
181
  });
241
182
  }
@@ -199,6 +199,81 @@ export function HtmlAfterSwitchInChildren() @{
199
199
  </ContentWrapper>
200
200
  }
201
201
 
202
+ // A component whose root is a single @if (root-controlled: no `<!>` wrapper),
203
+ // used as the first child of a composite slot with following siblings — exercises
204
+ // the hydration boundary advance for a root-controlled block inside a composite.
205
+ function IfHeading({ primary, children }: { primary: boolean; children: any }) @{
206
+ @if (primary) {
207
+ <h1 class="heading">{children}</h1>
208
+ } @else {
209
+ <h2 class="heading">{children}</h2>
210
+ }
211
+ }
212
+
213
+ export function HtmlAfterIfInChildren() @{
214
+ <ContentWrapper>
215
+ <IfHeading primary={true}>{'Title'}</IfHeading>
216
+ <p>{'First paragraph'}</p>
217
+ <p>{'Second paragraph'}</p>
218
+ <CodeBlock code="const x = 1;" />
219
+ <p>{'After code'}</p>
220
+ </ContentWrapper>
221
+ }
222
+
223
+ // Root-controlled @for (sibling semantics via the ROOT_CONTROLLED flag) as a
224
+ // composite child with following siblings.
225
+ function ForList({ items }: { items: string[] }) @{
226
+ @for (const item of items) {
227
+ <span class="for-item">{item}</span>
228
+ }
229
+ }
230
+
231
+ export function HtmlAfterForInChildren() @{
232
+ <ContentWrapper>
233
+ <ForList items={['Title', 'Subtitle']} />
234
+ <p>{'First paragraph'}</p>
235
+ <CodeBlock code="const x = 1;" />
236
+ <p>{'After code'}</p>
237
+ </ContentWrapper>
238
+ }
239
+
240
+ // Root-controlled @try (async branches) as a composite child with siblings.
241
+ function TryBox({ value }: { value: string }) @{
242
+ @try {
243
+ <div class="try-box">{value}</div>
244
+ } @catch (e) {
245
+ <span>{'error'}</span>
246
+ }
247
+ }
248
+
249
+ export function HtmlAfterTryInChildren() @{
250
+ <ContentWrapper>
251
+ <TryBox value="Title" />
252
+ <p>{'First paragraph'}</p>
253
+ <CodeBlock code="const x = 1;" />
254
+ <p>{'After code'}</p>
255
+ </ContentWrapper>
256
+ }
257
+
258
+ // Root-controlled single component: a component whose body is a single child
259
+ // component (no `<!>` wrapper), used as a composite child with siblings.
260
+ function Boxed({ children }: { children: any }) @{
261
+ <span class="boxed">{children}</span>
262
+ }
263
+
264
+ function IndirectHeading({ text }: { text: string }) @{
265
+ <Boxed>{text}</Boxed>
266
+ }
267
+
268
+ export function HtmlAfterComponentInChildren() @{
269
+ <ContentWrapper>
270
+ <IndirectHeading text="Title" />
271
+ <p>{'First paragraph'}</p>
272
+ <CodeBlock code="const x = 1;" />
273
+ <p>{'After code'}</p>
274
+ </ContentWrapper>
275
+ }
276
+
202
277
  function NavItem(&{
203
278
  href,
204
279
  text: label,
@@ -147,6 +147,56 @@ describe('hydration > html tags', () => {
147
147
  expect(html).toContain('After code');
148
148
  });
149
149
 
150
+ it('hydrates html block after if-based component in children', async () => {
151
+ await hydrateComponent(
152
+ ServerComponents.HtmlAfterIfInChildren,
153
+ ClientComponents.HtmlAfterIfInChildren,
154
+ );
155
+ const html = container.innerHTML;
156
+ expect(html).toContain('Title');
157
+ expect(html).toContain('First paragraph');
158
+ expect(html).toContain('Second paragraph');
159
+ expect(html).toContain('const x = 1;');
160
+ expect(html).toContain('After code');
161
+ });
162
+
163
+ it('hydrates html block after for-based component in children', async () => {
164
+ await hydrateComponent(
165
+ ServerComponents.HtmlAfterForInChildren,
166
+ ClientComponents.HtmlAfterForInChildren,
167
+ );
168
+ const html = container.innerHTML;
169
+ expect(html).toContain('Title');
170
+ expect(html).toContain('Subtitle');
171
+ expect(html).toContain('First paragraph');
172
+ expect(html).toContain('const x = 1;');
173
+ expect(html).toContain('After code');
174
+ });
175
+
176
+ it('hydrates html block after try-based component in children', async () => {
177
+ await hydrateComponent(
178
+ ServerComponents.HtmlAfterTryInChildren,
179
+ ClientComponents.HtmlAfterTryInChildren,
180
+ );
181
+ const html = container.innerHTML;
182
+ expect(html).toContain('Title');
183
+ expect(html).toContain('First paragraph');
184
+ expect(html).toContain('const x = 1;');
185
+ expect(html).toContain('After code');
186
+ });
187
+
188
+ it('hydrates html block after single-component-root component in children', async () => {
189
+ await hydrateComponent(
190
+ ServerComponents.HtmlAfterComponentInChildren,
191
+ ClientComponents.HtmlAfterComponentInChildren,
192
+ );
193
+ const html = container.innerHTML;
194
+ expect(html).toContain('Title');
195
+ expect(html).toContain('First paragraph');
196
+ expect(html).toContain('const x = 1;');
197
+ expect(html).toContain('After code');
198
+ });
199
+
150
200
  it('hydrates layout with sidebar (if-blocks) followed by main sibling', async () => {
151
201
  await hydrateComponent(
152
202
  ServerComponents.LayoutWithSidebarAndMain,
@@ -192,7 +192,9 @@ export default function A() @{
192
192
 
193
193
  const result = compile(source, 'test.tsrx', { mode: 'server' }).code;
194
194
 
195
- expect(result).toContain(`_$_.output_push('Hello')`);
195
+ // Indented text is trimmed to `Hello` and emitted as output (not a dangling
196
+ // `"Hello";`); the server accumulator merges it into the parent element.
197
+ expect(result).toContain(`__out += '<div>Hello</div>'`);
196
198
  expect(result).not.toContain(`"Hello";`);
197
199
  });
198
200
 
@@ -275,7 +277,9 @@ export function App() @{
275
277
  { mode: 'server' },
276
278
  );
277
279
 
278
- expect(result.code).toContain(`_$_.output_push('Rock &amp; "Roll"')`);
280
+ // Entities decode then re-escape for text (`&amp;`→`&`→`&amp;`, `&quot;`→`"`);
281
+ // the server accumulator merges the result into the parent element.
282
+ expect(result.code).toContain(`__out += '<div>Rock &amp; "Roll"</div>'`);
279
283
  });
280
284
 
281
285
  it('emits anonymous component expressions as arrows in SSR output', () => {
package/types/index.d.ts CHANGED
@@ -69,6 +69,8 @@ export function tick(): Promise<void>;
69
69
 
70
70
  export function untrack<T>(fn: () => T): T;
71
71
 
72
+ export function snapshot<T extends Iterable<unknown> | object>(value: T): T;
73
+
72
74
  export function flushSync<T>(fn?: () => T): T;
73
75
 
74
76
  export function effect(fn: (() => void) | (() => () => void)): void;