ripple 0.2.28 → 0.2.29

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.28",
6
+ "version": "0.2.29",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -424,12 +424,22 @@ function RipplePlugin(config) {
424
424
  // This node is used for Prettier, we don't actually need
425
425
  // the node for Ripple's transform process
426
426
  element.children = [component.css];
427
+ // Ensure we escape JSX <tag></tag> context
428
+ const tokContexts = this.acornTypeScript.tokContexts;
429
+ const curContext = this.curContext();
430
+
431
+ if (curContext === tokContexts.tc_expr) {
432
+ this.context.pop();
433
+ }
434
+
427
435
  return element;
428
436
  } else {
437
+ this.enterScope(0);
429
438
  this.parseTemplateBody(element.children);
439
+ this.exitScope();
430
440
  }
441
+ // Ensure we escape JSX <tag></tag> context
431
442
  const tokContexts = this.acornTypeScript.tokContexts;
432
-
433
443
  const curContext = this.curContext();
434
444
 
435
445
  if (curContext === tokContexts.tc_expr) {
@@ -151,6 +151,13 @@ const visitors = {
151
151
  const { state, visit, path } = context;
152
152
 
153
153
  for (const declarator of node.declarations) {
154
+ if (is_inside_component(context) && node.kind === 'var') {
155
+ error(
156
+ 'var declarations are not allowed in components, use let or const instead',
157
+ state.analysis.module.filename,
158
+ declarator,
159
+ );
160
+ }
154
161
  const metadata = { tracking: false, await: false };
155
162
  const parent = path.at(-1);
156
163
  const init_is_untracked =
@@ -133,6 +133,7 @@ const visitors = {
133
133
  }
134
134
 
135
135
  if (
136
+ !is_inside_component(context, true) ||
136
137
  context.state.to_ts ||
137
138
  (parent?.type === 'MemberExpression' && parent.property === node) ||
138
139
  is_inside_call_expression(context) ||
@@ -164,6 +165,10 @@ const visitors = {
164
165
  context.state.metadata.tracking = true;
165
166
  }
166
167
 
168
+ if (!is_inside_component(context, true) || is_inside_call_expression(context)) {
169
+ return context.next();
170
+ }
171
+
167
172
  return b.call(
168
173
  '$.with_scope',
169
174
  b.id('__block'),
@@ -572,7 +577,18 @@ const visitors = {
572
577
  );
573
578
  }
574
579
 
575
- transform_children(node.children, { visit, state, root: false });
580
+ const init = [];
581
+ const update = [];
582
+
583
+ transform_children(node.children, { visit, state: { ...state, init, update }, root: false });
584
+
585
+ if (init.length > 0) {
586
+ state.init.push(b.block(init));
587
+ }
588
+
589
+ if (update.length > 0) {
590
+ state.init.push(b.stmt(b.call('$.render', b.thunk(b.block(update)))));
591
+ }
576
592
 
577
593
  state.template.push(`</${node.id.name}>`);
578
594
  } else {
@@ -1258,7 +1274,7 @@ function transform_ts_child(node, context) {
1258
1274
  .filter((attr) => {
1259
1275
  if (attr.type === 'UseAttribute') {
1260
1276
  use_attributes.push(attr);
1261
- return false; // Filter out from JSX attributes
1277
+ return false;
1262
1278
  }
1263
1279
  return true;
1264
1280
  })
@@ -1415,7 +1431,12 @@ function transform_ts_child(node, context) {
1415
1431
  ),
1416
1432
  ),
1417
1433
  );
1434
+ } else if (node.type === 'Component') {
1435
+ const component = visit(node, context.state);
1436
+
1437
+ state.init.push(component);
1418
1438
  } else {
1439
+ debugger;
1419
1440
  throw new Error('TODO');
1420
1441
  }
1421
1442
  }
@@ -327,15 +327,16 @@ export function build_hoisted_params(node, context) {
327
327
  return params;
328
328
  }
329
329
 
330
- export function is_inside_component(context) {
330
+ export function is_inside_component(context, includes_functions = false) {
331
331
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
332
332
  const context_node = context.path[i];
333
333
  const type = context_node.type;
334
334
 
335
335
  if (
336
- type === 'FunctionExpression' ||
337
- type === 'ArrowFunctionExpression' ||
338
- type === 'FunctionDeclaration'
336
+ !includes_functions &&
337
+ (type === 'FunctionExpression' ||
338
+ type === 'ArrowFunctionExpression' ||
339
+ type === 'FunctionDeclaration')
339
340
  ) {
340
341
  return false;
341
342
  }
@@ -42,6 +42,8 @@ export { RippleArray } from './array.js';
42
42
 
43
43
  export { RippleSet } from './set.js';
44
44
 
45
+ export { RippleMap } from './map.js';
46
+
45
47
  export { keyed } from './internal/client/for.js';
46
48
 
47
49
  export { user_effect as effect } from './internal/client/blocks.js';
@@ -0,0 +1,147 @@
1
+ import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
2
+
3
+ const introspect_methods = ['entries', 'forEach', 'values', Symbol.iterator];
4
+
5
+ let init = false;
6
+
7
+ export class RippleMap extends Map {
8
+ #tracked_size;
9
+ #tracked_items = new Map();
10
+
11
+ constructor(iterable) {
12
+ super();
13
+
14
+ var block = scope();
15
+
16
+ if (iterable) {
17
+ for (var [key, value] of iterable) {
18
+ super.set(key, value);
19
+ this.#tracked_items.set(key, tracked(0, block));
20
+ }
21
+ }
22
+
23
+ this.#tracked_size = tracked(this.size, block);
24
+
25
+ if (!init) {
26
+ init = true;
27
+ this.#init();
28
+ }
29
+ }
30
+
31
+ #init() {
32
+ var proto = RippleMap.prototype;
33
+ var map_proto = Map.prototype;
34
+
35
+ for (const method of introspect_methods) {
36
+ proto[method] = function (...v) {
37
+ this.$size;
38
+ this.#read_all();
39
+
40
+ return map_proto[method].apply(this, v);
41
+ };
42
+ }
43
+ }
44
+
45
+ get(key) {
46
+ var tracked_items = this.#tracked_items;
47
+ var t = tracked_items.get(key);
48
+
49
+ if (t === undefined) {
50
+ // same logic as has
51
+ this.$size;
52
+ } else {
53
+ get(t);
54
+ }
55
+
56
+ return super.get(key);
57
+ }
58
+
59
+ has(key) {
60
+ var has = super.has(key);
61
+ var tracked_items = this.#tracked_items;
62
+ var t = tracked_items.get(key);
63
+
64
+ if (t === undefined) {
65
+ // if no tracked it also means super didn't have it
66
+ // It's not possible to have a disconnect, we tract each key
67
+ // If the key doesn't exist, track the size in case it's added later
68
+ // but don't create tracked entries willy-nilly to track all possible keys
69
+ this.$size;
70
+ } else {
71
+ get(t);
72
+ }
73
+
74
+ return has;
75
+ }
76
+
77
+ set(key, value) {
78
+ var block = scope();
79
+ var tracked_items = this.#tracked_items;
80
+ var t = tracked_items.get(key);
81
+ var prev_res = super.get(key);
82
+
83
+ super.set(key, value);
84
+
85
+ if (!t) {
86
+ tracked_items.set(key, tracked(0, block));
87
+ set(this.#tracked_size, this.size, block);
88
+ } else if (prev_res !== value) {
89
+ increment(t, block);
90
+ }
91
+
92
+ return this;
93
+ }
94
+
95
+ delete(key) {
96
+ var block = scope();
97
+ var tracked_items = this.#tracked_items;
98
+ var t = tracked_items.get(key);
99
+ var result = super.delete(key);
100
+
101
+ if (t) {
102
+ increment(t, block);
103
+ tracked_items.delete(key);
104
+ set(this.#tracked_size, this.size, block);
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ clear() {
111
+ var block = scope();
112
+
113
+ if (super.size === 0) {
114
+ return;
115
+ }
116
+
117
+ for (var [_, t] of this.#tracked_items) {
118
+ increment(t, block);
119
+ }
120
+
121
+ super.clear();
122
+ this.#tracked_items.clear();
123
+ set(this.#tracked_size, 0, block);
124
+ }
125
+
126
+ keys() {
127
+ this.$size;
128
+ return super.keys();
129
+ }
130
+
131
+ #read_all() {
132
+ for (const [, t] of this.#tracked_items) {
133
+ get(t);
134
+ }
135
+ }
136
+
137
+ get $size() {
138
+ return get(this.#tracked_size);
139
+ }
140
+
141
+ toJSON() {
142
+ this.$size;
143
+ this.#read_all();
144
+
145
+ return [...this];
146
+ }
147
+ }
@@ -1,25 +1,10 @@
1
1
  import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
2
2
 
3
- const introspect_methods = [
4
- 'entries',
5
- 'forEach',
6
- 'keys',
7
- 'values',
8
- Symbol.iterator
9
- ];
10
-
11
- const compare_other_methods = [
12
- 'isDisjointFrom',
13
- 'isSubsetOf',
14
- 'isSupersetOf',
15
- ];
16
-
17
- const new_other_methods = [
18
- 'difference',
19
- 'intersection',
20
- 'symmetricDifference',
21
- 'union',
22
- ];
3
+ const introspect_methods = ['entries', 'forEach', 'keys', 'values', Symbol.iterator];
4
+
5
+ const compare_other_methods = ['isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
6
+
7
+ const new_other_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
23
8
 
24
9
  let init = false;
25
10
 
@@ -91,9 +76,7 @@ export class RippleSet extends Set {
91
76
  other.$size;
92
77
  }
93
78
 
94
- return new RippleSet(
95
- set_proto[method].apply(this, [other, ...v])
96
- );
79
+ return new RippleSet(set_proto[method].apply(this, [other, ...v]));
97
80
  };
98
81
  }
99
82
  }
@@ -113,21 +96,17 @@ export class RippleSet extends Set {
113
96
  delete(value) {
114
97
  var block = scope();
115
98
 
116
- if (super.has(value)) {
117
- super.delete(value);
118
- var t = this.#tracked_items.get(value);
119
-
120
- if (t) {
121
- increment(t, block);
122
- }
99
+ if (!super.delete(value)) {
100
+ return false;
101
+ }
123
102
 
124
- this.#tracked_items.delete(value);
125
- set(this.#tracked_size, this.size, block);
103
+ var t = this.#tracked_items.get(value);
126
104
 
127
- return true;
128
- }
105
+ increment(t, block);
106
+ this.#tracked_items.delete(value);
107
+ set(this.#tracked_size, this.size, block);
129
108
 
130
- return false;
109
+ return true;
131
110
  }
132
111
 
133
112
  has(value) {
@@ -137,34 +116,32 @@ export class RippleSet extends Set {
137
116
  var t = tracked_items.get(value);
138
117
 
139
118
  if (t === undefined) {
140
- if (!has) {
141
- // If the value doesn't exist, track the size in case it's added later
142
- // but don't create tracked entries willy-nilly to track all possible values
143
- this.$size;
144
-
145
- return false;
146
- }
147
-
148
- t = tracked(0, block);
149
- tracked_items.set(value, t);
119
+ // if no tracked it also means super didn't have it
120
+ // It's not possible to have a disconnect, we track each value
121
+ // If the value doesn't exist, track the size in case it's added later
122
+ // but don't create tracked entries willy-nilly to track all possible values
123
+ this.$size;
124
+ } else {
125
+ get(t);
150
126
  }
151
127
 
152
- get(t);
153
128
  return has;
154
129
  }
155
130
 
156
131
  clear() {
157
132
  var block = scope();
158
133
 
159
- if (this.size > 0) {
160
- for (var [value, t] of this.#tracked_items) {
161
- increment(t, block);
162
- }
134
+ if (super.size === 0) {
135
+ return;
136
+ }
163
137
 
164
- super.clear();
165
- this.#tracked_items.clear();
166
- set(this.#tracked_size, 0, block);
138
+ for (var [_, t] of this.#tracked_items) {
139
+ increment(t, block);
167
140
  }
141
+
142
+ super.clear();
143
+ this.#tracked_items.clear();
144
+ set(this.#tracked_size, 0, block);
168
145
  }
169
146
 
170
147
  get $size() {
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+
3
+ import { mount, flushSync, effect } from 'ripple';
4
+
5
+ describe('passing reactivity between boundaries tests', () => {
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('can pass reactivity between functions with simple arrays and destructuring', () => {
25
+ let log: string[] = [];
26
+
27
+ function createDouble([ $count ]) {
28
+ const $double = $count * 2;
29
+
30
+ effect(() => {
31
+ log.push('Count:' + $count);
32
+ });
33
+
34
+ return [ $double ];
35
+ }
36
+
37
+ component App() {
38
+ let $count = 0;
39
+
40
+ const [ $double ] = createDouble([ $count ]);
41
+
42
+ <div>{'Double: ' + $double}</div>
43
+ <button onClick={() => { $count++; }}>{'Increment'}</button>
44
+ }
45
+
46
+ render(App);
47
+ flushSync();
48
+
49
+ expect(container.querySelector('div').textContent).toBe('Double: 0');
50
+ expect(log).toEqual(['Count:0']);
51
+
52
+ const button = container.querySelector('button');
53
+
54
+ button.click();
55
+ flushSync();
56
+
57
+ expect(container.querySelector('div').textContent).toBe('Double: 2');
58
+ expect(log).toEqual(['Count:0', 'Count:1']);
59
+
60
+ button.click();
61
+ flushSync();
62
+
63
+ expect(container.querySelector('div').textContent).toBe('Double: 4');
64
+ expect(log).toEqual(['Count:0', 'Count:1', 'Count:2']);
65
+ });
66
+
67
+ it('can pass reactivity between functions with simple objects and destructuring', () => {
68
+ let log: string[] = [];
69
+
70
+ function createDouble({ $count }) {
71
+ const $double = $count * 2;
72
+
73
+ effect(() => {
74
+ log.push('Count:' + $count);
75
+ });
76
+
77
+ return { $double };
78
+ }
79
+
80
+ component App() {
81
+ let $count = 0;
82
+
83
+ const { $double } = createDouble({ $count });
84
+
85
+ <div>{'Double: ' + $double}</div>
86
+ <button onClick={() => { $count++; }}>{'Increment'}</button>
87
+ }
88
+
89
+ render(App);
90
+ flushSync();
91
+
92
+ expect(container.querySelector('div').textContent).toBe('Double: 0');
93
+ expect(log).toEqual(['Count:0']);
94
+
95
+ const button = container.querySelector('button');
96
+
97
+ button.click();
98
+ flushSync();
99
+
100
+ expect(container.querySelector('div').textContent).toBe('Double: 2');
101
+ expect(log).toEqual(['Count:0', 'Count:1']);
102
+
103
+ button.click();
104
+ flushSync();
105
+
106
+ expect(container.querySelector('div').textContent).toBe('Double: 4');
107
+ expect(log).toEqual(['Count:0', 'Count:1', 'Count:2']);
108
+ });
109
+ });
@@ -58,4 +58,25 @@ describe('compiler success tests', () => {
58
58
  render(App);
59
59
  });
60
60
 
61
+ it('render lexical blocks without crashing', () => {
62
+ component App() {
63
+ <div>
64
+ const a = 1;
65
+ <div>
66
+ const b = 1;
67
+ </div>
68
+ <div>
69
+ const b = 1;
70
+ </div>
71
+ </div>
72
+ <div>
73
+ const a = 2;
74
+ <div>
75
+ const b = 1;
76
+ </div>
77
+ </div>
78
+ }
79
+
80
+ render(App);
81
+ })
61
82
  });
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, RippleMap } from 'ripple';
3
+
4
+ describe('RippleMap', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('handles set with update and delete operations with a reactive $size var', () => {
24
+ component MapTest() {
25
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
26
+ let $value = map.get('a');
27
+ let $size = map.$size;
28
+
29
+ <button onClick={() => map.set('d', 4)}>{'set'}</button>
30
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
31
+ <button onClick={() => map.set('a', 5)}>{'update'}</button>
32
+
33
+ <pre>{map.get('d')}</pre>
34
+ <pre>{$size}</pre>
35
+ <pre>{$value}</pre>
36
+ }
37
+
38
+ render(MapTest);
39
+
40
+ const setButton = container.querySelectorAll('button')[0];
41
+ const deleteButton = container.querySelectorAll('button')[1];
42
+ const updateButton = container.querySelectorAll('button')[2];
43
+
44
+ setButton.click();
45
+ flushSync();
46
+
47
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
48
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
49
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
50
+
51
+ deleteButton.click();
52
+ flushSync();
53
+
54
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
55
+
56
+ updateButton.click();
57
+ flushSync();
58
+
59
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('5');
60
+ });
61
+
62
+ it('handles clear operation', () => {
63
+ component MapTest() {
64
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
65
+
66
+ <button onClick={() => map.clear()}>{'clear'}</button>
67
+ <pre>{map.$size}</pre>
68
+ }
69
+
70
+ render(MapTest);
71
+
72
+ const clearButton = container.querySelector('button');
73
+
74
+ clearButton.click();
75
+ flushSync();
76
+
77
+ expect(container.querySelector('pre').textContent).toBe('0');
78
+ });
79
+
80
+ it('handles has operation and tracks reactive $has', () => {
81
+ component MapTest() {
82
+ let map = new RippleMap([['a', 1], ['b', 2], ['c', 3]]);
83
+ let $has = map.has('b');
84
+
85
+ <button onClick={() => map.delete('b')}>{'delete'}</button>
86
+ <pre>{$has}</pre>
87
+ }
88
+
89
+ render(MapTest);
90
+
91
+ const deleteButton = container.querySelector('button');
92
+ expect(container.querySelector('pre').textContent).toBe('true');
93
+
94
+ deleteButton.click();
95
+ flushSync();
96
+
97
+ expect(container.querySelector('pre').textContent).toBe('false');
98
+ });
99
+
100
+ it('handles reactivity of keys, values, and entries', () => {
101
+ component MapTest() {
102
+ let map = new RippleMap([['x', 10], ['y', 20]]);
103
+ let $keys = Array.from(map.keys());
104
+ let $values = Array.from(map.values());
105
+ let $entries = Array.from(map.entries());
106
+
107
+ <button onClick={() => map.delete('x')}>{'delete'}</button>
108
+
109
+ <pre>{JSON.stringify($keys)}</pre>
110
+ <pre>{JSON.stringify($values)}</pre>
111
+ <pre>{JSON.stringify($entries)}</pre>
112
+ }
113
+
114
+ render(MapTest);
115
+
116
+ const deleteButton = container.querySelector('button');
117
+
118
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["x","y"]');
119
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[10,20]');
120
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["x",10],["y",20]]');
121
+
122
+ deleteButton.click();
123
+ flushSync();
124
+
125
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["y"]');
126
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('[20]');
127
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('[["y",20]]');
128
+ });
129
+
130
+ it('toJSON returns correct object', () => {
131
+ let map = new RippleMap([['foo', 1], ['bar', 2]]);
132
+ expect(JSON.stringify(map)).toEqual('[["foo",1],["bar",2]]');
133
+ });
134
+ });
@@ -82,7 +82,7 @@ describe('RippleSet', () => {
82
82
  let $hasValue = items.has(2);
83
83
 
84
84
  <button onClick={() => items.delete(2)}>{'delete'}</button>
85
- <pre>{JSON.stringify($hasValue)}</pre>
85
+ <pre>{$hasValue}</pre>
86
86
  }
87
87
 
88
88
  render(SetTest);
package/types/index.d.ts CHANGED
@@ -50,3 +50,8 @@ export class RippleSet<T> extends Set<T> {
50
50
  union(other: RippleSet<T> | Set<T>): RippleSet<T>;
51
51
  toJSON(): T[];
52
52
  }
53
+
54
+ export class RippleMap<K, V> extends Map<K, V> {
55
+ get $size(): number;
56
+ toJSON(): [K, V][];
57
+ }
File without changes