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 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.8",
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.17",
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 tok = this.acornTypeScript.tokContexts;
394
+ const tokContexts = this.acornTypeScript.tokContexts;
395
395
 
396
396
  const curContext = this.curContext();
397
397
 
398
- if (curContext === tok.tc_expr) {
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
- binding !== null &&
168
- is_tracked_name(declarator.id.name) &&
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
- mark_as_tracked(path);
170
+ mark_as_tracked(path);
174
171
 
175
- visit(declarator, { ...state, metadata });
172
+ visit(declarator, { ...state, metadata });
176
173
 
177
- if (init_is_untracked && metadata.tracking) {
178
- metadata.tracking = false;
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
- binding.transform = {
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(b.call('Object.keys', tracked_element.argument)));
879
+ tracked.push(b.spread(tracked_element.argument));
880
880
  elements.push(tracked_element);
881
881
  } else {
882
882
  elements.push(tracked_element);
@@ -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 (object === left && transform?.assign) {
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(object, value);
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
@@ -38,4 +38,4 @@ export { user_effect as effect } from './internal/client/blocks.js';
38
38
 
39
39
  export { Portal } from './internal/client/portal.js';
40
40
 
41
- export { ref } from './internal/client/runtime.js';
41
+ export { ref } from './internal/client/runtime.js';
@@ -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
 
@@ -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
+ });
@@ -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
  });
@@ -28,6 +28,7 @@ describe('composite components', () => {
28
28
 
29
29
  component App() {
30
30
  let $count = 0;
31
+
31
32
  <button onClick={() => $count++}>{'Increment'}</button>
32
33
  <Button $count={$count} />
33
34
  }
@@ -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}`)}>{"Add Item"}</button>
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
- const button = container.querySelector('button');
53
- button.click();
54
- flushSync();
52
+ const button = container.querySelector('button');
55
53
 
56
- expect(container).toMatchSnapshot();
54
+ button.click();
55
+ flushSync();
56
+
57
+ expect(container).toMatchSnapshot();
57
58
  });
58
59
  });
@@ -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>;