ripple 0.2.183 → 0.2.185
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/package.json +2 -2
- package/src/compiler/phases/2-analyze/index.js +7 -2
- package/src/compiler/phases/3-transform/client/index.js +181 -131
- package/src/compiler/phases/3-transform/segments.js +93 -49
- package/src/compiler/phases/3-transform/server/index.js +135 -45
- package/src/compiler/scope.js +11 -16
- package/src/compiler/types/index.d.ts +333 -90
- package/src/compiler/types/parse.d.ts +127 -9
- package/src/runtime/index-server.js +10 -27
- package/src/runtime/internal/client/operations.js +1 -1
- package/src/runtime/internal/client/runtime.js +8 -8
- package/src/runtime/internal/client/types.d.ts +5 -5
- package/src/runtime/internal/server/index.js +268 -17
- package/src/runtime/internal/server/types.d.ts +19 -11
- package/tests/client/switch.test.ripple +73 -23
- package/tests/server/basic.test.ripple +119 -0
- package/tests/server/composite.test.ripple +1 -1
- package/tests/server/context.test.ripple +31 -0
- package/tests/server/switch.test.ripple +21 -0
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import type { Context } from './context.js';
|
|
2
2
|
|
|
3
3
|
export type Component = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
c: null | Map<Context<any>, any>;
|
|
5
|
+
p: null | Component;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export type Dependency = {
|
|
9
|
+
c: number;
|
|
10
|
+
t: Tracked | Derived;
|
|
11
|
+
n: null | Dependency;
|
|
12
|
+
};
|
|
8
13
|
|
|
9
14
|
export type Derived = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
a: { get?: Function; set?: Function };
|
|
16
|
+
c: number;
|
|
17
|
+
co: null | Component;
|
|
18
|
+
d: null | Dependency;
|
|
19
|
+
f: number;
|
|
20
|
+
fn: Function;
|
|
21
|
+
v: any;
|
|
15
22
|
};
|
|
16
23
|
|
|
17
24
|
export type Tracked = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
a: { get?: Function; set?: Function };
|
|
26
|
+
c: number;
|
|
27
|
+
f: number;
|
|
28
|
+
v: any;
|
|
29
|
+
};
|
|
@@ -5,8 +5,8 @@ describe('switch statements', () => {
|
|
|
5
5
|
component App() {
|
|
6
6
|
let value = track('b');
|
|
7
7
|
|
|
8
|
-
<button onClick={() => @value = 'c'}>{'Change to C'}</button>
|
|
9
|
-
<button onClick={() => @value = 'a'}>{'Change to A'}</button>
|
|
8
|
+
<button onClick={() => (@value = 'c')}>{'Change to C'}</button>
|
|
9
|
+
<button onClick={() => (@value = 'a')}>{'Change to A'}</button>
|
|
10
10
|
|
|
11
11
|
switch (@value) {
|
|
12
12
|
case 'a':
|
|
@@ -24,15 +24,16 @@ describe('switch statements', () => {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
render(App);
|
|
27
|
-
expect(container.textContent).toBe('
|
|
27
|
+
expect(container.querySelector('div').textContent).toBe('Case B');
|
|
28
28
|
|
|
29
|
-
container.querySelectorAll('button')[0].click();
|
|
29
|
+
container.querySelectorAll('button')[0].click();
|
|
30
30
|
flushSync();
|
|
31
|
-
expect(container.textContent).toBe('Change to CChange to ACase C');
|
|
32
31
|
|
|
33
|
-
container.
|
|
32
|
+
expect(container.querySelector('div').textContent).toBe('Case C');
|
|
33
|
+
container.querySelectorAll('button')[1].click();
|
|
34
34
|
flushSync();
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
expect(container.querySelector('div').textContent).toBe('Case A');
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
it('renders switch with reactive discriminant', () => {
|
|
@@ -54,22 +55,24 @@ describe('switch statements', () => {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
render(App);
|
|
57
|
-
expect(container.textContent).toBe('
|
|
58
|
+
expect(container.querySelector('div').textContent).toBe('Count is 1');
|
|
58
59
|
|
|
59
60
|
container.querySelector('button').click();
|
|
60
61
|
flushSync();
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
expect(container.querySelector('div').textContent).toBe('Count is 2');
|
|
62
64
|
|
|
63
65
|
container.querySelector('button').click();
|
|
64
66
|
flushSync();
|
|
65
|
-
|
|
67
|
+
|
|
68
|
+
expect(container.querySelector('div').textContent).toBe('Count is other');
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
it('renders switch with default clause only', () => {
|
|
69
72
|
component App() {
|
|
70
73
|
let value = track('x');
|
|
71
74
|
|
|
72
|
-
<button onClick={() => @value = 'y'}>{'Change Value'}</button>
|
|
75
|
+
<button onClick={() => (@value = 'y')}>{'Change Value'}</button>
|
|
73
76
|
|
|
74
77
|
switch (@value) {
|
|
75
78
|
default:
|
|
@@ -78,11 +81,54 @@ describe('switch statements', () => {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
render(App);
|
|
81
|
-
expect(container.textContent).toBe('
|
|
84
|
+
expect(container.querySelector('div').textContent).toBe('Default for x');
|
|
82
85
|
|
|
83
86
|
container.querySelector('button').click();
|
|
84
87
|
flushSync();
|
|
85
|
-
|
|
88
|
+
|
|
89
|
+
expect(container.querySelector('div').textContent).toBe('Default for y');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders switch using fallthrough without recreating DOM unnecessarily', () => {
|
|
93
|
+
component App() {
|
|
94
|
+
let value = track('a');
|
|
95
|
+
|
|
96
|
+
<button onClick={() => (@value = 'b')}>{'Change to B'}</button>
|
|
97
|
+
<button onClick={() => (@value = 'c')}>{'Change to C'}</button>
|
|
98
|
+
|
|
99
|
+
switch (@value) {
|
|
100
|
+
case 'a':
|
|
101
|
+
<div>{'Case A'}</div>
|
|
102
|
+
break;
|
|
103
|
+
case 'b':
|
|
104
|
+
case 'c':
|
|
105
|
+
<div>{'Case B or C'}</div>
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
<div>{'Default Case'}</div>
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render(App);
|
|
113
|
+
const [buttonB, buttonC] = container.querySelectorAll('button');
|
|
114
|
+
|
|
115
|
+
expect(container.querySelector('div').textContent).toContain('Case A');
|
|
116
|
+
|
|
117
|
+
buttonB.click();
|
|
118
|
+
flushSync();
|
|
119
|
+
|
|
120
|
+
expect(container.querySelector('div').textContent).toBe('Case B or C');
|
|
121
|
+
|
|
122
|
+
buttonC.click();
|
|
123
|
+
flushSync();
|
|
124
|
+
|
|
125
|
+
expect(container.querySelector('div').textContent).toBe('Case B or C');
|
|
126
|
+
|
|
127
|
+
container.querySelector('div').textContent = 'DOM check';
|
|
128
|
+
buttonB.click();
|
|
129
|
+
flushSync();
|
|
130
|
+
|
|
131
|
+
expect(container.querySelector('div').textContent).toBe('DOM check');
|
|
86
132
|
});
|
|
87
133
|
|
|
88
134
|
it('renders switch with template content and JS logic', () => {
|
|
@@ -90,9 +136,9 @@ describe('switch statements', () => {
|
|
|
90
136
|
let status = track('active');
|
|
91
137
|
let message = track('');
|
|
92
138
|
|
|
93
|
-
<button onClick={() => @status = 'pending'}>{'Pending'}</button>
|
|
94
|
-
<button onClick={() => @status = 'completed'}>{'Completed'}</button>
|
|
95
|
-
<button onClick={() => @status = 'error'}>{'Error'}</button>
|
|
139
|
+
<button onClick={() => (@status = 'pending')}>{'Pending'}</button>
|
|
140
|
+
<button onClick={() => (@status = 'completed')}>{'Completed'}</button>
|
|
141
|
+
<button onClick={() => (@status = 'error')}>{'Error'}</button>
|
|
96
142
|
|
|
97
143
|
switch (@status) {
|
|
98
144
|
case 'active':
|
|
@@ -114,20 +160,24 @@ describe('switch statements', () => {
|
|
|
114
160
|
}
|
|
115
161
|
|
|
116
162
|
render(App);
|
|
117
|
-
|
|
163
|
+
const [buttonPending, buttonCompleted, buttonError] = container.querySelectorAll('button');
|
|
164
|
+
expect(container.querySelector('div').textContent).toBe('Status: Currently active.');
|
|
118
165
|
|
|
119
|
-
|
|
166
|
+
buttonPending.click();
|
|
120
167
|
flushSync();
|
|
121
|
-
expect(container.textContent).toBe('PendingCompletedErrorStatus: Waiting for completion.');
|
|
122
168
|
|
|
123
|
-
container.
|
|
169
|
+
expect(container.querySelector('div').textContent).toBe('Status: Waiting for completion.');
|
|
170
|
+
|
|
171
|
+
buttonCompleted.click();
|
|
124
172
|
flushSync();
|
|
125
|
-
|
|
173
|
+
|
|
174
|
+
expect(container.querySelector('div').textContent).toBe('Status: Task finished!');
|
|
126
175
|
expect(container.querySelector('.success')).toBeTruthy();
|
|
127
176
|
|
|
128
|
-
|
|
177
|
+
buttonError.click();
|
|
129
178
|
flushSync();
|
|
130
|
-
|
|
179
|
+
|
|
180
|
+
expect(container.querySelector('div').textContent).toBe('Status: An error occurred.');
|
|
131
181
|
expect(container.querySelector('.error')).toBeTruthy();
|
|
132
182
|
});
|
|
133
183
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { render } from 'ripple/server';
|
|
3
|
+
import { track } from 'ripple';
|
|
3
4
|
|
|
4
5
|
describe('basic client', () => {
|
|
5
6
|
it('render static text', async () => {
|
|
@@ -12,4 +13,122 @@ describe('basic client', () => {
|
|
|
12
13
|
expect(head).toBe('');
|
|
13
14
|
expect(body).toBe('<div>Hello World</div>');
|
|
14
15
|
});
|
|
16
|
+
|
|
17
|
+
it('renders tracked state updates', async () => {
|
|
18
|
+
component Counter() {
|
|
19
|
+
const count = track(0);
|
|
20
|
+
|
|
21
|
+
@count++;
|
|
22
|
+
@count = @count + 5;
|
|
23
|
+
|
|
24
|
+
<div>{@count}</div>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { body } = await render(Counter);
|
|
28
|
+
|
|
29
|
+
expect(body).toBe('<div>6</div>');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders dynamic component with tracked props', async () => {
|
|
33
|
+
component Child({ count, ...rest }) {
|
|
34
|
+
<div {...rest}>{'Child Component'}</div>
|
|
35
|
+
<div>{@count}</div>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
component Parent() {
|
|
39
|
+
const count = track(10);
|
|
40
|
+
let Dynamic = track(() => Child);
|
|
41
|
+
|
|
42
|
+
<@Dynamic {count} class={{ test: true }} />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { body } = await render(Parent);
|
|
46
|
+
|
|
47
|
+
expect(body).toBe('<div class="test">Child Component</div><div>10</div>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders tracked object properties', async () => {
|
|
51
|
+
component ObjCounter() {
|
|
52
|
+
const obj = { count: track(2) };
|
|
53
|
+
|
|
54
|
+
obj.@count += 3;
|
|
55
|
+
obj.@count = obj.@count + 1;
|
|
56
|
+
|
|
57
|
+
<div>{obj.@count}</div>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { body } = await render(ObjCounter);
|
|
61
|
+
|
|
62
|
+
expect(body).toBe('<div>6</div>');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders spread props with tracked values', async () => {
|
|
66
|
+
component SpreadProps() {
|
|
67
|
+
const id = track('unique-id');
|
|
68
|
+
const isActive = track(true);
|
|
69
|
+
const styles = track({ color: 'red', fontSize: '16px' });
|
|
70
|
+
|
|
71
|
+
<div {...{ id: @id, class: { active: @isActive }, style: @styles }}>{'Spread Props'}</div>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { body } = await render(SpreadProps);
|
|
75
|
+
|
|
76
|
+
expect(body).toBe(
|
|
77
|
+
'<div id="unique-id" class="active" style="color: red; font-size: 16px;">Spread Props</div>',
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('handles AssignExpressions with tracked values or properties correctly', async () => {
|
|
82
|
+
component Assignments() {
|
|
83
|
+
const count = track(0);
|
|
84
|
+
const obj = { value: track(5) };
|
|
85
|
+
|
|
86
|
+
@count += 10;
|
|
87
|
+
obj.@value *= 2;
|
|
88
|
+
|
|
89
|
+
<div>{@count}</div>
|
|
90
|
+
<div>{obj.@value}</div>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { body } = await render(Assignments);
|
|
94
|
+
|
|
95
|
+
expect(body).toBe('<div>10</div><div>10</div>');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it(`handles derived changes via tracked dependencies' changes`, async () => {
|
|
99
|
+
component Derived() {
|
|
100
|
+
let base = track(5);
|
|
101
|
+
let multiplier = track(3);
|
|
102
|
+
const derived = track(() => @base * @multiplier);
|
|
103
|
+
|
|
104
|
+
<div>{@derived}</div>
|
|
105
|
+
|
|
106
|
+
@base += 2;
|
|
107
|
+
|
|
108
|
+
<div>{@derived}</div>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { body } = await render(Derived);
|
|
112
|
+
|
|
113
|
+
expect(body).toBe('<div>15</div><div>21</div>');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it(`handles derived changes based on another derived's dependencies' changes`, async () => {
|
|
117
|
+
component NestedDerived() {
|
|
118
|
+
let a = track(2);
|
|
119
|
+
let b = track(3);
|
|
120
|
+
const sum = track(() => @a + @b);
|
|
121
|
+
const product = track(() => @sum * 2);
|
|
122
|
+
|
|
123
|
+
<div>{@product}</div>
|
|
124
|
+
|
|
125
|
+
@a = 4;
|
|
126
|
+
|
|
127
|
+
<div>{@product}</div>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { body } = await render(NestedDerived);
|
|
131
|
+
|
|
132
|
+
expect(body).toBe('<div>10</div><div>14</div>');
|
|
133
|
+
});
|
|
15
134
|
});
|
|
@@ -76,7 +76,7 @@ describe('generics', () => {
|
|
|
76
76
|
|
|
77
77
|
// 14. Generic with constraint + default
|
|
78
78
|
type Extractor<T extends { id: number } = { id: number }> = (v: T) => number;
|
|
79
|
-
const m: Extractor = (v) => v.id;
|
|
79
|
+
const m: Extractor<{ id: number }> = (v) => v.id;
|
|
80
80
|
|
|
81
81
|
// 15. Generic in angle after "new" + trailing call
|
|
82
82
|
class Wrapper<T> {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render } from 'ripple/server';
|
|
3
|
+
import { Context } from 'ripple';
|
|
4
|
+
|
|
5
|
+
describe('Context API', () => {
|
|
6
|
+
it('handles context override in nested components', async () => {
|
|
7
|
+
const MessageContext = new Context('default');
|
|
8
|
+
|
|
9
|
+
component Inner() {
|
|
10
|
+
const msg = MessageContext.get();
|
|
11
|
+
<span>{msg}</span>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
component Middle() {
|
|
15
|
+
MessageContext.set('middle');
|
|
16
|
+
<div>
|
|
17
|
+
<Inner />
|
|
18
|
+
</div>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
component Outer() {
|
|
22
|
+
MessageContext.set('outer');
|
|
23
|
+
<Middle />
|
|
24
|
+
<Inner />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { body } = await render(Outer);
|
|
28
|
+
|
|
29
|
+
expect(body).toBe('<div><span>middle</span></div><span>outer</span>');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -24,4 +24,25 @@ describe('SSR: switch statements', () => {
|
|
|
24
24
|
const { body } = await render(App);
|
|
25
25
|
expect(body).toBe('<div>Case B</div>');
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
it('renders switch using fallthrough case', async () => {
|
|
29
|
+
component App() {
|
|
30
|
+
let value = 'b';
|
|
31
|
+
|
|
32
|
+
switch (value) {
|
|
33
|
+
case 'a':
|
|
34
|
+
<div>{'Case A'}</div>
|
|
35
|
+
break;
|
|
36
|
+
case 'b':
|
|
37
|
+
case 'c':
|
|
38
|
+
<div>{'Case B or C'}</div>
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
<div>{'Default Case'}</div>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { body } = await render(App);
|
|
46
|
+
expect(body).toBe('<div>Case B or C</div>');
|
|
47
|
+
});
|
|
27
48
|
});
|