ripple 0.3.82 → 0.3.83
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.
- package/CHANGELOG.md +40 -0
- package/package.json +3 -3
- package/tests/client/basic/basic.rendering.test.tsrx +41 -0
- package/tests/client/return.test.tsrx +103 -0
- package/tests/hydration/compiled/client/basic.js +1 -1
- package/tests/hydration/compiled/client/if-children.js +1 -1
- package/tests/hydration/compiled/client/return.js +0 -2
- package/tests/hydration/compiled/server/basic.js +1 -1
- package/tests/hydration/compiled/server/if-children.js +1 -1
- package/tests/hydration/compiled/server/return.js +12 -26
- package/tests/server/basic.test.tsrx +19 -0
- package/tests/setup-client.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.83
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1269](https://github.com/Ripple-TS/ripple/pull/1269)
|
|
8
|
+
[`8747e8f`](https://github.com/Ripple-TS/ripple/commit/8747e8f306628443d3c4d73bce0d79e986f5966e)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Treat plain JS control flow
|
|
10
|
+
inside `@{ … }` as ordinary JavaScript that returns JSX.
|
|
11
|
+
|
|
12
|
+
Only `@`-directives (`@if`/`@for`/`@switch`/`@try`) lower to template control
|
|
13
|
+
flow. Plain `if`/`for`/`for…of`/`for…in`/`while`/`do…while`/`switch`/`try`
|
|
14
|
+
inside a code block are now compiled exactly like the same control flow in a
|
|
15
|
+
regular `function C() { …; return <jsx> }` body — their JSX returns become
|
|
16
|
+
`tsrx_element` values rather than being template-ized.
|
|
17
|
+
|
|
18
|
+
Previously these plain statements were mis-routed into the template transform:
|
|
19
|
+
on **ripple** an early-return guard produced a `_$_.if`/`_$_.switch`/`_$_.try`
|
|
20
|
+
wrapper (with dead code in the `switch`/`try` cases) and plain loops threw a
|
|
21
|
+
compile error; on **solid** they produced
|
|
22
|
+
`<Show>`/`<Switch>`/`<For>`/`<Errored>` (dropping trailing output for `try`).
|
|
23
|
+
They now stay as plain control flow, so early-return guards and loops behave
|
|
24
|
+
like normal JavaScript.
|
|
25
|
+
|
|
26
|
+
As part of this, the ripple client and server targets no longer emit the
|
|
27
|
+
`return_guard` bookkeeping variable: a plain early `return` is a real early
|
|
28
|
+
return, so subsequent template output is naturally skipped without a guard flag.
|
|
29
|
+
|
|
30
|
+
On **solid**, this means a plain guard (`if (signal()) return …`) inside a
|
|
31
|
+
component body now runs once at setup — exactly like a regular Solid component —
|
|
32
|
+
instead of being lifted into a reactive `<Show>`. Use `@if` (or another
|
|
33
|
+
`@`-directive) when you want reactive conditional rendering.
|
|
34
|
+
|
|
35
|
+
- Updated dependencies
|
|
36
|
+
[[`3d93339`](https://github.com/Ripple-TS/ripple/commit/3d93339e851818b547c43c29c8965700c069b037),
|
|
37
|
+
[`5646eb4`](https://github.com/Ripple-TS/ripple/commit/5646eb4e4c101b34100acf30ea57ad4065a47720),
|
|
38
|
+
[`8747e8f`](https://github.com/Ripple-TS/ripple/commit/8747e8f306628443d3c4d73bce0d79e986f5966e),
|
|
39
|
+
[`8747e8f`](https://github.com/Ripple-TS/ripple/commit/8747e8f306628443d3c4d73bce0d79e986f5966e)]:
|
|
40
|
+
- @tsrx/ripple@0.1.31
|
|
41
|
+
- @tsrx/core@0.1.31
|
|
42
|
+
|
|
3
43
|
## 0.3.82
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.83",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"clsx": "^2.1.1",
|
|
74
74
|
"devalue": "^5.8.1",
|
|
75
75
|
"esm-env": "^1.2.2",
|
|
76
|
-
"@tsrx/core": "0.1.
|
|
77
|
-
"@tsrx/ripple": "0.1.
|
|
76
|
+
"@tsrx/core": "0.1.31",
|
|
77
|
+
"@tsrx/ripple": "0.1.31"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/estree": "^1.0.8",
|
|
@@ -235,6 +235,47 @@ second
|
|
|
235
235
|
]);
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
+
it('never prints nullish (null/undefined) in interpolated text', () => {
|
|
239
|
+
function Basic() @{
|
|
240
|
+
const nothing = null;
|
|
241
|
+
const missing = undefined;
|
|
242
|
+
<>
|
|
243
|
+
<div class="null">value: {nothing}</div>
|
|
244
|
+
<div class="undefined">value: {missing}</div>
|
|
245
|
+
<div class="explicit">raw: {String(nothing)}</div>
|
|
246
|
+
</>
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
render(Basic);
|
|
250
|
+
|
|
251
|
+
expect(container.querySelector('.null').textContent).toBe('value: ');
|
|
252
|
+
expect(container.querySelector('.undefined').textContent).toBe('value: ');
|
|
253
|
+
// An explicit `String(...)` is the author's own coercion and is left
|
|
254
|
+
// untouched, so it still stringifies nullish to "null".
|
|
255
|
+
expect(container.querySelector('.explicit').textContent).toBe('raw: null');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('reactively clears interpolated text when a value becomes nullish', () => {
|
|
259
|
+
function Basic() @{
|
|
260
|
+
let &[name] = track<string | null>('Ada');
|
|
261
|
+
<>
|
|
262
|
+
<div class="label">Name: {name}</div>
|
|
263
|
+
<button onClick={() => (name = name == null ? 'Ada' : null)}>toggle</button>
|
|
264
|
+
</>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
render(Basic);
|
|
268
|
+
expect(container.querySelector('.label').textContent).toBe('Name: Ada');
|
|
269
|
+
|
|
270
|
+
container.querySelector('button').click();
|
|
271
|
+
flushSync();
|
|
272
|
+
expect(container.querySelector('.label').textContent).toBe('Name: ');
|
|
273
|
+
|
|
274
|
+
container.querySelector('button').click();
|
|
275
|
+
flushSync();
|
|
276
|
+
expect(container.querySelector('.label').textContent).toBe('Name: Ada');
|
|
277
|
+
});
|
|
278
|
+
|
|
238
279
|
it('does not render unreachable statements after an ASI return', () => {
|
|
239
280
|
function Basic() {
|
|
240
281
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { track, flushSync, type Tracked } from 'ripple';
|
|
2
3
|
import { compile } from '@tsrx/ripple';
|
|
3
4
|
|
|
4
5
|
describe('returns in restricted scopes', () => {
|
|
@@ -15,6 +16,24 @@ describe('returns in restricted scopes', () => {
|
|
|
15
16
|
).toThrow('Return statements are not allowed at the top level of a module.');
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
it('throws error when return is used inside @try/@catch/@pending blocks', () => {
|
|
20
|
+
for (const source of [
|
|
21
|
+
`function App() @{
|
|
22
|
+
@try { return <div>{'ok'}</div>; } @catch (e) { <div>{'err'}</div> }
|
|
23
|
+
}`,
|
|
24
|
+
`function App() @{
|
|
25
|
+
@try { <div>{'ok'}</div> } @catch (e) { return <div>{'err'}</div>; }
|
|
26
|
+
}`,
|
|
27
|
+
`function App() @{
|
|
28
|
+
@try { <div>{'ok'}</div> } @pending { return <div>{'load'}</div>; } @catch (e) { <div>{'err'}</div> }
|
|
29
|
+
}`,
|
|
30
|
+
]) {
|
|
31
|
+
expect(() => compile(source, 'App.tsrx', { mode: 'client' })).toThrow(
|
|
32
|
+
'Return statements are not allowed inside TSRX templates',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
18
37
|
it('allows returns inside regular functions declared in TSRX templates', () => {
|
|
19
38
|
expect(
|
|
20
39
|
() => compile(
|
|
@@ -132,4 +151,88 @@ describe('function returns in client components', () => {
|
|
|
132
151
|
render(App);
|
|
133
152
|
expect(container.textContent).toBe('hello');
|
|
134
153
|
});
|
|
154
|
+
|
|
155
|
+
it('renders a template directive branch and siblings from a returned fragment', () => {
|
|
156
|
+
function App() {
|
|
157
|
+
const ready = true;
|
|
158
|
+
return <>
|
|
159
|
+
@if (ready) {
|
|
160
|
+
<div class="yielded">yielded</div>
|
|
161
|
+
}
|
|
162
|
+
<span class="outer">outer</span>
|
|
163
|
+
</>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
render(App);
|
|
167
|
+
expect(container.querySelector('.yielded')?.textContent).toBe('yielded');
|
|
168
|
+
expect(container.querySelector('.outer')?.textContent).toBe('outer');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('renders a template directive branch inside a returned single element', () => {
|
|
172
|
+
function App() {
|
|
173
|
+
const ready = true;
|
|
174
|
+
return <div class="root">
|
|
175
|
+
@if (ready) {
|
|
176
|
+
<p class="inner">inner</p>
|
|
177
|
+
}
|
|
178
|
+
<span class="outer">outer</span>
|
|
179
|
+
</div>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render(App);
|
|
183
|
+
expect(container.querySelector('.inner')?.textContent).toBe('inner');
|
|
184
|
+
expect(container.querySelector('.outer')?.textContent).toBe('outer');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('renders a returned @for loop body alongside siblings', () => {
|
|
188
|
+
function App() {
|
|
189
|
+
const items = [1, 2, 3];
|
|
190
|
+
return <>
|
|
191
|
+
@for (const item of items) {
|
|
192
|
+
<li class="item">{item}</li>
|
|
193
|
+
}
|
|
194
|
+
<span class="outer">outer</span>
|
|
195
|
+
</>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
render(App);
|
|
199
|
+
expect([...container.querySelectorAll('.item')].map((n) => n.textContent)).toEqual([
|
|
200
|
+
'1',
|
|
201
|
+
'2',
|
|
202
|
+
'3',
|
|
203
|
+
]);
|
|
204
|
+
expect(container.querySelector('.outer')?.textContent).toBe('outer');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('reactively toggles a returned @if branch driven by a tracked prop', () => {
|
|
208
|
+
function Dashboard({ user }: { user: Tracked<string | null> }) {
|
|
209
|
+
return <>
|
|
210
|
+
@if (!user.value) {
|
|
211
|
+
<p class="empty">No user found</p>
|
|
212
|
+
}
|
|
213
|
+
<h1 class="welcome">Welcome,{user.value}</h1>
|
|
214
|
+
</>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function App() {
|
|
218
|
+
let &[user, userTracked] = track<string | null>(null);
|
|
219
|
+
return <>
|
|
220
|
+
<Dashboard user={userTracked} />
|
|
221
|
+
<button onClick={() => (user = user === 'Adam' ? null : 'Adam')}>Toggle</button>
|
|
222
|
+
</>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
render(App);
|
|
226
|
+
expect(container.querySelector('.empty')?.textContent).toBe('No user found');
|
|
227
|
+
expect(container.querySelector('.welcome')?.textContent).toBe('Welcome,');
|
|
228
|
+
|
|
229
|
+
container.querySelector('button')?.click();
|
|
230
|
+
flushSync();
|
|
231
|
+
expect(container.querySelector('.empty')).toBeNull();
|
|
232
|
+
expect(container.querySelector('.welcome')?.textContent).toBe('Welcome,Adam');
|
|
233
|
+
|
|
234
|
+
container.querySelector('button')?.click();
|
|
235
|
+
flushSync();
|
|
236
|
+
expect(container.querySelector('.empty')?.textContent).toBe('No user found');
|
|
237
|
+
});
|
|
135
238
|
});
|
|
@@ -196,7 +196,7 @@ export function Greeting(props) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
_$_.render(() => {
|
|
199
|
-
_$_.set_text(expression, 'Hello ' + _$_.with_scope(__block, () => String(props.name)));
|
|
199
|
+
_$_.set_text(expression, 'Hello ' + _$_.with_scope(__block, () => String(props.name ?? '')));
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
_$_.append(__anchor, div_6);
|
|
@@ -384,7 +384,7 @@ export function DomChildrenThenStaticSiblings() {
|
|
|
384
384
|
_$_.next();
|
|
385
385
|
|
|
386
386
|
_$_.render(() => {
|
|
387
|
-
_$_.set_text(expression_3, 'Item count: ' + _$_.with_scope(__block, () => String(lazy_6.value)));
|
|
387
|
+
_$_.set_text(expression_3, 'Item count: ' + _$_.with_scope(__block, () => String(lazy_6.value ?? '')));
|
|
388
388
|
});
|
|
389
389
|
|
|
390
390
|
_$_.append(__anchor, fragment_9, true);
|
|
@@ -6,7 +6,6 @@ var root_1 = _$_.template(`<div class="ready">ready</div>`, 1, 1);
|
|
|
6
6
|
|
|
7
7
|
export function GuardReturnRenders() {
|
|
8
8
|
return _$_.tsrx_element((__anchor, __block) => {
|
|
9
|
-
var return_guard = false;
|
|
10
9
|
const ready = true;
|
|
11
10
|
|
|
12
11
|
if (!ready) {
|
|
@@ -21,7 +20,6 @@ export function GuardReturnRenders() {
|
|
|
21
20
|
|
|
22
21
|
export function GuardReturnNull() {
|
|
23
22
|
return _$_.tsrx_element((__anchor, __block) => {
|
|
24
|
-
var return_guard = false;
|
|
25
23
|
const ready = false;
|
|
26
24
|
|
|
27
25
|
if (!ready) {
|
|
@@ -513,7 +513,7 @@ export function DomChildrenThenStaticSiblings() {
|
|
|
513
513
|
_$_.output_push('>');
|
|
514
514
|
|
|
515
515
|
{
|
|
516
|
-
_$_.output_push(_$_.escape('Item count: ' + String(lazy_6.value)));
|
|
516
|
+
_$_.output_push(_$_.escape('Item count: ' + String(lazy_6.value ?? '')));
|
|
517
517
|
}
|
|
518
518
|
|
|
519
519
|
_$_.output_push('</li>');
|
|
@@ -3,7 +3,6 @@ import * as _$_ from 'ripple/internal/server';
|
|
|
3
3
|
|
|
4
4
|
export function GuardReturnRenders() {
|
|
5
5
|
return _$_.tsrx_element(() => {
|
|
6
|
-
var return_guard = false;
|
|
7
6
|
const ready = true;
|
|
8
7
|
|
|
9
8
|
if (!ready) {
|
|
@@ -11,28 +10,21 @@ export function GuardReturnRenders() {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
_$_.regular_block(() => {
|
|
14
|
-
_$_.output_push('
|
|
13
|
+
_$_.output_push('<div');
|
|
14
|
+
_$_.output_push(' class="ready"');
|
|
15
|
+
_$_.output_push('>');
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
_$_.output_push('
|
|
18
|
-
_$_.output_push(' class="ready"');
|
|
19
|
-
_$_.output_push('>');
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
_$_.output_push('ready');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
_$_.output_push('</div>');
|
|
17
|
+
{
|
|
18
|
+
_$_.output_push('ready');
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
_$_.output_push('
|
|
21
|
+
_$_.output_push('</div>');
|
|
29
22
|
});
|
|
30
23
|
});
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
export function GuardReturnNull() {
|
|
34
27
|
return _$_.tsrx_element(() => {
|
|
35
|
-
var return_guard = false;
|
|
36
28
|
const ready = false;
|
|
37
29
|
|
|
38
30
|
if (!ready) {
|
|
@@ -40,21 +32,15 @@ export function GuardReturnNull() {
|
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
_$_.regular_block(() => {
|
|
43
|
-
_$_.output_push('
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
_$_.output_push('<div');
|
|
47
|
-
_$_.output_push(' class="ready"');
|
|
48
|
-
_$_.output_push('>');
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
_$_.output_push('ready');
|
|
52
|
-
}
|
|
35
|
+
_$_.output_push('<div');
|
|
36
|
+
_$_.output_push(' class="ready"');
|
|
37
|
+
_$_.output_push('>');
|
|
53
38
|
|
|
54
|
-
|
|
39
|
+
{
|
|
40
|
+
_$_.output_push('ready');
|
|
55
41
|
}
|
|
56
42
|
|
|
57
|
-
_$_.output_push('
|
|
43
|
+
_$_.output_push('</div>');
|
|
58
44
|
});
|
|
59
45
|
});
|
|
60
46
|
}
|
|
@@ -24,6 +24,25 @@ describe('basic client', () => {
|
|
|
24
24
|
expect(body).toBeHtml('<div><span>Not HTML</span></div>');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
it('never prints nullish (null/undefined) in interpolated text', async () => {
|
|
28
|
+
function Basic() @{
|
|
29
|
+
const nothing = null;
|
|
30
|
+
const missing = undefined;
|
|
31
|
+
<>
|
|
32
|
+
<div class="null">value: {nothing}</div>
|
|
33
|
+
<div class="undefined">value: {missing}</div>
|
|
34
|
+
<div class="explicit">raw: {String(nothing)}</div>
|
|
35
|
+
</>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { body } = await render(Basic);
|
|
39
|
+
|
|
40
|
+
expect(body).toBeHtml(
|
|
41
|
+
'<div class="null">value: </div>' + '<div class="undefined">value: </div>' +
|
|
42
|
+
'<div class="explicit">raw: null</div>',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
27
46
|
it('renders direct JSX text children as text', async () => {
|
|
28
47
|
function Basic() @{
|
|
29
48
|
<>
|
package/tests/setup-client.js
CHANGED