ripple 0.2.8 → 0.2.10
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 +3 -2
- package/src/compiler/phases/1-parse/index.js +2 -2
- package/src/compiler/phases/2-analyze/index.js +46 -28
- package/src/compiler/phases/3-transform/index.js +3 -3
- package/src/compiler/utils.js +10 -11
- package/src/runtime/index.js +1 -1
- package/src/runtime/internal/client/runtime.js +6 -0
- package/src/utils/builders.js +2 -2
- package/tests/array.test.ripple +54 -0
- package/tests/basic.test.ripple +45 -0
- package/tests/composite.test.ripple +1 -0
- package/tests/for.test.ripple +6 -5
- package/tests/ref.test.ripple +3 -1
- package/types/index.d.ts +7 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is a TypeScript UI framework for the web",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.10",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index.js",
|
|
9
9
|
"main": "src/runtime/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/trueadm/ripple/issues"
|
|
17
17
|
},
|
|
18
|
+
"homepage": "https://ripplejs.com",
|
|
18
19
|
"keywords": [
|
|
19
20
|
"ripple",
|
|
20
21
|
"UI",
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
"acorn-typescript": "^1.4.13",
|
|
54
55
|
"esrap": "^2.1.0",
|
|
55
56
|
"is-reference": "^3.0.3",
|
|
56
|
-
"magic-string": "^0.30.
|
|
57
|
+
"magic-string": "^0.30.18",
|
|
57
58
|
"muggle-string": "^0.4.1",
|
|
58
59
|
"zimmerframe": "^1.1.2"
|
|
59
60
|
},
|
|
@@ -391,11 +391,11 @@ function RipplePlugin(config) {
|
|
|
391
391
|
} else {
|
|
392
392
|
this.parseTemplateBody(element.children);
|
|
393
393
|
}
|
|
394
|
-
const
|
|
394
|
+
const tokContexts = this.acornTypeScript.tokContexts;
|
|
395
395
|
|
|
396
396
|
const curContext = this.curContext();
|
|
397
397
|
|
|
398
|
-
if (curContext ===
|
|
398
|
+
if (curContext === tokContexts.tc_expr) {
|
|
399
399
|
this.context.pop();
|
|
400
400
|
}
|
|
401
401
|
}
|
|
@@ -163,39 +163,57 @@ const visitors = {
|
|
|
163
163
|
if (declarator.id.type === 'Identifier') {
|
|
164
164
|
const binding = state.scope.get(declarator.id.name);
|
|
165
165
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
parent?.type !== 'ForOfStatement'
|
|
170
|
-
) {
|
|
171
|
-
binding.kind = 'tracked';
|
|
166
|
+
if (binding !== null && parent?.type !== 'ForOfStatement') {
|
|
167
|
+
if (is_tracked_name(declarator.id.name)) {
|
|
168
|
+
binding.kind = 'tracked';
|
|
172
169
|
|
|
173
|
-
|
|
170
|
+
mark_as_tracked(path);
|
|
174
171
|
|
|
175
|
-
|
|
172
|
+
visit(declarator, { ...state, metadata });
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
if (init_is_untracked && metadata.tracking) {
|
|
175
|
+
metadata.tracking = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
binding.transform = {
|
|
179
|
+
read: (node) => {
|
|
180
|
+
return metadata.tracking && !metadata.await
|
|
181
|
+
? b.call('$.get_computed', node)
|
|
182
|
+
: b.call('$.get_tracked', node);
|
|
183
|
+
},
|
|
184
|
+
assign: (node, value) => {
|
|
185
|
+
return b.call('$.set', node, value, b.id('__block'));
|
|
186
|
+
},
|
|
187
|
+
update: (node) => {
|
|
188
|
+
return b.call(
|
|
189
|
+
node.prefix ? '$.update_pre' : '$.update',
|
|
190
|
+
node.argument,
|
|
191
|
+
b.id('__block'),
|
|
192
|
+
node.operator === '--' && b.literal(-1),
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
} else if (binding.initial?.type !== 'Literal') {
|
|
197
|
+
for (const ref of binding.references) {
|
|
198
|
+
const path = ref.path;
|
|
199
|
+
const parent_node = path?.at(-1);
|
|
200
|
+
|
|
201
|
+
// We're reading a computed property, which might mean it's a reactive property
|
|
202
|
+
if (parent_node?.type === 'MemberExpression' && parent_node.computed) {
|
|
203
|
+
binding.transform = {
|
|
204
|
+
assign: (node, value, computed) => {
|
|
205
|
+
if (!computed) {
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
return b.call('$.set_property', node, visit(computed), value, b.id('__block'));
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
179
214
|
}
|
|
180
215
|
|
|
181
|
-
|
|
182
|
-
read: (node) => {
|
|
183
|
-
return metadata.tracking && !metadata.await
|
|
184
|
-
? b.call('$.get_computed', node)
|
|
185
|
-
: b.call('$.get_tracked', node);
|
|
186
|
-
},
|
|
187
|
-
assign: (node, value) => {
|
|
188
|
-
return b.call('$.set', node, value, b.id('__block'));
|
|
189
|
-
},
|
|
190
|
-
update: (node) => {
|
|
191
|
-
return b.call(
|
|
192
|
-
node.prefix ? '$.update_pre' : '$.update',
|
|
193
|
-
node.argument,
|
|
194
|
-
b.id('__block'),
|
|
195
|
-
node.operator === '--' && b.literal(-1),
|
|
196
|
-
);
|
|
197
|
-
},
|
|
198
|
-
};
|
|
216
|
+
visit(declarator, state);
|
|
199
217
|
} else {
|
|
200
218
|
visit(declarator, state);
|
|
201
219
|
}
|
|
@@ -71,7 +71,7 @@ function build_getter(node, context) {
|
|
|
71
71
|
const binding = state.scope.get(node.name);
|
|
72
72
|
|
|
73
73
|
// don't transform the declaration itself
|
|
74
|
-
if (node !== binding?.node && binding?.transform) {
|
|
74
|
+
if (node !== binding?.node && binding?.transform?.read) {
|
|
75
75
|
return binding.transform.read(node, context.state?.metadata?.spread);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -806,7 +806,7 @@ const visitors = {
|
|
|
806
806
|
const binding = context.state.scope.get(left.name);
|
|
807
807
|
const transformers = left && binding?.transform;
|
|
808
808
|
|
|
809
|
-
if (left === argument
|
|
809
|
+
if (left === argument) {
|
|
810
810
|
if (transformers?.update) {
|
|
811
811
|
return transformers.update(node);
|
|
812
812
|
} else if (binding.kind === 'prop') {
|
|
@@ -876,7 +876,7 @@ const visitors = {
|
|
|
876
876
|
const tracked_element = context.visit(element, { ...context.state, metadata });
|
|
877
877
|
|
|
878
878
|
if (metadata.tracking) {
|
|
879
|
-
tracked.push(b.spread(
|
|
879
|
+
tracked.push(b.spread(tracked_element.argument));
|
|
880
880
|
elements.push(tracked_element);
|
|
881
881
|
} else {
|
|
882
882
|
elements.push(tracked_element);
|
package/src/compiler/utils.js
CHANGED
|
@@ -364,13 +364,7 @@ export function is_inside_call_expression(context) {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
export function is_tracked_name(name) {
|
|
367
|
-
return (
|
|
368
|
-
typeof name === 'string' &&
|
|
369
|
-
name.startsWith('$') &&
|
|
370
|
-
name.length > 1 &&
|
|
371
|
-
name[1] !== '$' &&
|
|
372
|
-
name !== '$length'
|
|
373
|
-
);
|
|
367
|
+
return typeof name === 'string' && name.startsWith('$') && name.length > 1 && name[1] !== '$';
|
|
374
368
|
}
|
|
375
369
|
|
|
376
370
|
export function is_svelte_import(callee, context) {
|
|
@@ -492,15 +486,20 @@ export function build_assignment(operator, left, right, context) {
|
|
|
492
486
|
|
|
493
487
|
const transform = binding.transform;
|
|
494
488
|
|
|
495
|
-
const path = context.path.map((node) => node.type);
|
|
496
|
-
|
|
497
489
|
// reassignment
|
|
498
|
-
if (
|
|
490
|
+
if (
|
|
491
|
+
(object === left || (left.type === 'MemberExpression' && left.computed && operator === '=')) &&
|
|
492
|
+
transform?.assign
|
|
493
|
+
) {
|
|
499
494
|
let value = /** @type {Expression} */ (
|
|
500
495
|
context.visit(build_assignment_value(operator, left, right))
|
|
501
496
|
);
|
|
502
497
|
|
|
503
|
-
return transform.assign(
|
|
498
|
+
return transform.assign(
|
|
499
|
+
object,
|
|
500
|
+
value,
|
|
501
|
+
left.type === 'MemberExpression' && left.computed ? left.property : undefined,
|
|
502
|
+
);
|
|
504
503
|
}
|
|
505
504
|
|
|
506
505
|
// mutation
|
package/src/runtime/index.js
CHANGED
|
@@ -741,6 +741,12 @@ export function set_property(obj, property, value, block) {
|
|
|
741
741
|
var tracked = tracked_properties?.[property];
|
|
742
742
|
|
|
743
743
|
if (tracked === undefined) {
|
|
744
|
+
// Handle computed assignments to arrays
|
|
745
|
+
if (obj.$length && tracked_properties !== undefined && is_array(obj)) {
|
|
746
|
+
with_scope(block, () => {
|
|
747
|
+
obj.splice(property, 1, value);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
744
750
|
return res;
|
|
745
751
|
}
|
|
746
752
|
|
package/src/utils/builders.js
CHANGED
|
@@ -655,7 +655,7 @@ export function try_builder(block, handler = null, finalizer = null) {
|
|
|
655
655
|
type: 'TryStatement',
|
|
656
656
|
block,
|
|
657
657
|
handler,
|
|
658
|
-
finalizer
|
|
658
|
+
finalizer,
|
|
659
659
|
};
|
|
660
660
|
}
|
|
661
661
|
|
|
@@ -668,7 +668,7 @@ export function catch_clause_builder(param, body) {
|
|
|
668
668
|
return {
|
|
669
669
|
type: 'CatchClause',
|
|
670
670
|
param,
|
|
671
|
-
body
|
|
671
|
+
body,
|
|
672
672
|
};
|
|
673
673
|
}
|
|
674
674
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mount, flushSync, array } from 'ripple';
|
|
4
|
+
|
|
5
|
+
describe('array', () => {
|
|
6
|
+
let container;
|
|
7
|
+
|
|
8
|
+
function render(component) {
|
|
9
|
+
mount(component, {
|
|
10
|
+
target: container
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
container = document.createElement('div');
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.removeChild(container);
|
|
21
|
+
container = null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles direct assignment', () => {
|
|
25
|
+
component ArrayTest() {
|
|
26
|
+
let items = array(1, 2, 3);
|
|
27
|
+
|
|
28
|
+
<button onClick={() => items[items.$length] = items.$length + 1}>{'increment'}</button>
|
|
29
|
+
|
|
30
|
+
<Child items={items} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
component Child({ items }) {
|
|
34
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
35
|
+
<pre>{items.$length}</pre>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(ArrayTest);
|
|
39
|
+
|
|
40
|
+
const button = container.querySelector('button');
|
|
41
|
+
|
|
42
|
+
button.click();
|
|
43
|
+
flushSync();
|
|
44
|
+
|
|
45
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[1,2,3,4]');
|
|
46
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
|
|
47
|
+
|
|
48
|
+
button.click();
|
|
49
|
+
flushSync();
|
|
50
|
+
|
|
51
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[1,2,3,4,5]');
|
|
52
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('5');
|
|
53
|
+
});
|
|
54
|
+
});
|
package/tests/basic.test.ripple
CHANGED
|
@@ -43,6 +43,7 @@ describe('basic', () => {
|
|
|
43
43
|
it('render semi-dynamic text', () => {
|
|
44
44
|
component Basic() {
|
|
45
45
|
let text = 'Hello World';
|
|
46
|
+
|
|
46
47
|
<div>{text}</div>
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -54,6 +55,7 @@ describe('basic', () => {
|
|
|
54
55
|
it('render dynamic text', () => {
|
|
55
56
|
component Basic() {
|
|
56
57
|
let $text = 'Hello World';
|
|
58
|
+
|
|
57
59
|
<button onClick={() => $text = 'Hello Ripple'}>{'Change Text'}</button>
|
|
58
60
|
<div>{$text}</div>
|
|
59
61
|
}
|
|
@@ -71,6 +73,7 @@ describe('basic', () => {
|
|
|
71
73
|
it('render dynamic class attribute', () => {
|
|
72
74
|
component Basic() {
|
|
73
75
|
let $active = false;
|
|
76
|
+
|
|
74
77
|
<button onClick={() => $active = !$active}>{'Toggle'}</button>
|
|
75
78
|
<div $class={$active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
|
|
76
79
|
}
|
|
@@ -96,6 +99,7 @@ describe('basic', () => {
|
|
|
96
99
|
it('render dynamic id attribute', () => {
|
|
97
100
|
component Basic() {
|
|
98
101
|
let $count = 0;
|
|
102
|
+
|
|
99
103
|
<button onClick={() => $count++}>{'Increment'}</button>
|
|
100
104
|
<div $id={`item-${$count}`}>{'Dynamic ID'}</div>
|
|
101
105
|
}
|
|
@@ -121,6 +125,7 @@ describe('basic', () => {
|
|
|
121
125
|
it('render dynamic style attribute', () => {
|
|
122
126
|
component Basic() {
|
|
123
127
|
let $color = 'red';
|
|
128
|
+
|
|
124
129
|
<button onClick={() => $color = $color === 'red' ? 'blue' : 'red'}>{'Change Color'}</button>
|
|
125
130
|
<div $style={`color: ${$color}; font-weight: bold;`}>{'Dynamic Style'}</div>
|
|
126
131
|
}
|
|
@@ -144,6 +149,7 @@ describe('basic', () => {
|
|
|
144
149
|
component Basic() {
|
|
145
150
|
let $disabled = false;
|
|
146
151
|
let $checked = false;
|
|
152
|
+
|
|
147
153
|
<button onClick={() => {
|
|
148
154
|
$disabled = !$disabled;
|
|
149
155
|
$checked = !$checked;
|
|
@@ -170,6 +176,7 @@ describe('basic', () => {
|
|
|
170
176
|
component Basic() {
|
|
171
177
|
let $theme = 'light';
|
|
172
178
|
let $size = 'medium';
|
|
179
|
+
|
|
173
180
|
<button
|
|
174
181
|
onClick={() => {
|
|
175
182
|
$theme = $theme === 'light' ? 'dark' : 'light';
|
|
@@ -200,6 +207,7 @@ describe('basic', () => {
|
|
|
200
207
|
component Basic() {
|
|
201
208
|
let $showTitle = false;
|
|
202
209
|
let $showAria = false;
|
|
210
|
+
|
|
203
211
|
<button onClick={() => {
|
|
204
212
|
$showTitle = !$showTitle;
|
|
205
213
|
$showAria = !$showAria;
|
|
@@ -237,6 +245,7 @@ describe('basic', () => {
|
|
|
237
245
|
class: 'initial',
|
|
238
246
|
id: 'test-1'
|
|
239
247
|
};
|
|
248
|
+
|
|
240
249
|
<button
|
|
241
250
|
onClick={() => {
|
|
242
251
|
$attrs = {
|
|
@@ -299,4 +308,40 @@ describe('basic', () => {
|
|
|
299
308
|
|
|
300
309
|
expect(container.querySelector('.count').textContent).toBe('0');
|
|
301
310
|
});
|
|
311
|
+
|
|
312
|
+
it('renders multiple reactive lexical blocks with complexity', () => {
|
|
313
|
+
component Basic() {
|
|
314
|
+
const count = '$count';
|
|
315
|
+
|
|
316
|
+
<div>
|
|
317
|
+
let obj = {
|
|
318
|
+
$count: 0
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
<span>{obj[count]}</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div>
|
|
324
|
+
let b = {
|
|
325
|
+
$count: 0
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
<button onClick={() => b[count]--}>{'-'}</button>
|
|
329
|
+
<span class='count'>{b[count]}</span>
|
|
330
|
+
<button onClick={() => b[count]++}>{'+'}</button>
|
|
331
|
+
</div>
|
|
332
|
+
}
|
|
333
|
+
render(Basic);
|
|
334
|
+
|
|
335
|
+
const buttons = container.querySelectorAll('button');
|
|
336
|
+
|
|
337
|
+
buttons[0].click();
|
|
338
|
+
flushSync();
|
|
339
|
+
|
|
340
|
+
expect(container.querySelector('.count').textContent).toBe('-1');
|
|
341
|
+
|
|
342
|
+
buttons[1].click();
|
|
343
|
+
flushSync();
|
|
344
|
+
|
|
345
|
+
expect(container.querySelector('.count').textContent).toBe('0');
|
|
346
|
+
});
|
|
302
347
|
});
|
package/tests/for.test.ripple
CHANGED
|
@@ -43,16 +43,17 @@ describe('for statements', () => {
|
|
|
43
43
|
<div class={item}>{item}</div>
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
<button onClick={() => items.push(`Item ${items.$length + 1}`)}>{
|
|
46
|
+
<button onClick={() => items.push(`Item ${items.$length + 1}`)}>{'Add Item'}</button>
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
render(App);
|
|
50
50
|
expect(container).toMatchSnapshot();
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
button.click();
|
|
54
|
-
flushSync();
|
|
52
|
+
const button = container.querySelector('button');
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
button.click();
|
|
55
|
+
flushSync();
|
|
56
|
+
|
|
57
|
+
expect(container).toMatchSnapshot();
|
|
57
58
|
});
|
|
58
59
|
});
|
package/tests/ref.test.ripple
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { mount, flushSync, ref } from 'ripple';
|
|
4
4
|
|
|
5
|
-
describe('ref', () => {
|
|
5
|
+
describe('ref()', () => {
|
|
6
6
|
let container;
|
|
7
7
|
|
|
8
8
|
function render(component) {
|
|
@@ -23,6 +23,7 @@ describe('ref', () => {
|
|
|
23
23
|
it('creates a reactive ref with initial value', () => {
|
|
24
24
|
component TestRef() {
|
|
25
25
|
let $count = 5;
|
|
26
|
+
|
|
26
27
|
<div><span id='count'>{$count}</span></div>
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -34,6 +35,7 @@ describe('ref', () => {
|
|
|
34
35
|
it('updates when ref value changes', () => {
|
|
35
36
|
component TestRef() {
|
|
36
37
|
let $count = 0;
|
|
38
|
+
|
|
37
39
|
<div>
|
|
38
40
|
<span id='count'>{$count}</span>
|
|
39
41
|
<button id='btn' onClick={() => $count++}>{'Increment'}</button>
|
package/types/index.d.ts
CHANGED
|
@@ -16,3 +16,10 @@ export interface Ref<T> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export declare function ref<T>(value: T): Ref<T>;
|
|
19
|
+
|
|
20
|
+
export interface RippleArray<T> extends Array<T> {
|
|
21
|
+
$length: number;
|
|
22
|
+
toJSON(): T[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare function array<T>(...elements: T[]): RippleArray<T>;
|