ripple 0.2.15 → 0.2.17

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.15",
6
+ "version": "0.2.17",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -1,6 +1,7 @@
1
1
  import * as acorn from 'acorn';
2
2
  import { tsPlugin } from 'acorn-typescript';
3
3
  import { parse_style } from './style.js';
4
+ import { walk } from 'zimmerframe';
4
5
 
5
6
  const parser = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }), RipplePlugin());
6
7
 
@@ -387,7 +388,10 @@ function RipplePlugin(config) {
387
388
  this.#path.pop();
388
389
  this.next();
389
390
  }
390
- return null;
391
+ // This node is used for Prettier, we don't actually need
392
+ // the node for Ripple's transform process
393
+ element.children = [component.css];
394
+ return element;
391
395
  } else {
392
396
  this.parseTemplateBody(element.children);
393
397
  }
@@ -419,7 +423,12 @@ function RipplePlugin(config) {
419
423
  const position = this.curPosition();
420
424
  this.startLoc = position;
421
425
  this.endLoc = position;
426
+ // Avoid triggering onComment handlers, as they will have
427
+ // already been triggered when parsing the subscript before
428
+ const onComment = this.options.onComment;
429
+ this.options.onComment = () => {};
422
430
  this.next();
431
+ this.options.onComment = onComment;
423
432
 
424
433
  return base;
425
434
  }
@@ -558,8 +567,111 @@ function RipplePlugin(config) {
558
567
  };
559
568
  }
560
569
 
570
+ /**
571
+ * Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
572
+ * to add them after the fact. They are needed in order to support `ripple-ignore` comments
573
+ * in JS code and so that `prettier-plugin-ripple` doesn't remove all comments when formatting.
574
+ * @param {string} source
575
+ * @param {CommentWithLocation[]} comments
576
+ * @param {number} index
577
+ */
578
+ function get_comment_handlers(source, comments, index = 0) {
579
+ return {
580
+ onComment: (block, value, start, end, start_loc, end_loc) => {
581
+ if (block && /\n/.test(value)) {
582
+ let a = start;
583
+ while (a > 0 && source[a - 1] !== '\n') a -= 1;
584
+
585
+ let b = a;
586
+ while (/[ \t]/.test(source[b])) b += 1;
587
+
588
+ const indentation = source.slice(a, b);
589
+ value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
590
+ }
591
+
592
+ comments.push({
593
+ type: block ? 'Block' : 'Line',
594
+ value,
595
+ start,
596
+ end,
597
+ loc: {
598
+ start: /** @type {import('acorn').Position} */ (start_loc),
599
+ end: /** @type {import('acorn').Position} */ (end_loc),
600
+ },
601
+ });
602
+ },
603
+ add_comments: (ast) => {
604
+ if (comments.length === 0) return;
605
+
606
+ comments = comments
607
+ .filter((comment) => comment.start >= index)
608
+ .map(({ type, value, start, end }) => ({ type, value, start, end }));
609
+
610
+ walk(ast, null, {
611
+ _(node, { next, path }) {
612
+ let comment;
613
+
614
+ while (comments[0] && comments[0].start < node.start) {
615
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
616
+ (node.leadingComments ||= []).push(comment);
617
+ }
618
+
619
+ next();
620
+
621
+ if (comments[0]) {
622
+ if (node.type === 'BlockStatement' && node.body.length === 0) {
623
+ if (comments[0].start < node.end && comments[0].end < node.end) {
624
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
625
+ (node.innerComments ||= []).push(comment);
626
+ return;
627
+ }
628
+ }
629
+ const parent = /** @type {any} */ (path.at(-1));
630
+
631
+ if (parent === undefined || node.end !== parent.end) {
632
+ const slice = source.slice(node.end, comments[0].start);
633
+ const is_last_in_body =
634
+ ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
635
+ parent.body.indexOf(node) === parent.body.length - 1) ||
636
+ (parent?.type === 'ArrayExpression' &&
637
+ parent.elements.indexOf(node) === parent.elements.length - 1) ||
638
+ (parent?.type === 'ObjectExpression' &&
639
+ parent.properties.indexOf(node) === parent.properties.length - 1);
640
+
641
+ if (is_last_in_body) {
642
+ // Special case: There can be multiple trailing comments after the last node in a block,
643
+ // and they can be separated by newlines
644
+ let end = node.end;
645
+
646
+ while (comments.length) {
647
+ const comment = comments[0];
648
+ if (parent && comment.start >= parent.end) break;
649
+
650
+ (node.trailingComments ||= []).push(comment);
651
+ comments.shift();
652
+ end = comment.end;
653
+ }
654
+ } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
655
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
656
+ }
657
+ }
658
+ }
659
+ },
660
+ });
661
+
662
+ // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
663
+ // Adding them ensures that we can later detect the end of the expression tag correctly.
664
+ if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
665
+ debugger;
666
+ (ast.trailingComments ||= []).push(...comments.splice(0));
667
+ }
668
+ },
669
+ };
670
+ }
671
+
561
672
  export function parse(source) {
562
673
  const comments = [];
674
+ const { onComment, add_comments } = get_comment_handlers(source, comments);
563
675
  let ast;
564
676
 
565
677
  try {
@@ -567,23 +679,13 @@ export function parse(source) {
567
679
  sourceType: 'module',
568
680
  ecmaVersion: 13,
569
681
  locations: true,
570
- onComment: (block, text, start, end, startLoc, endLoc) => {
571
- comments.push({
572
- type: block ? 'Block' : 'Line',
573
- value: text,
574
- start,
575
- end,
576
- loc: {
577
- start: startLoc,
578
- end: endLoc,
579
- },
580
- });
581
- },
682
+ onComment,
582
683
  });
583
684
  } catch (e) {
584
685
  throw e;
585
686
  }
586
687
 
587
- ast.comments = comments;
688
+ add_comments(ast);
689
+
588
690
  return ast;
589
691
  }
@@ -115,7 +115,13 @@ const visitors = {
115
115
  if (!context.state.to_ts && node.importKind === 'type') {
116
116
  return b.empty;
117
117
  }
118
- return context.next();
118
+
119
+ return {
120
+ ...node,
121
+ specifiers: node.specifiers
122
+ .filter((spec) => spec.importKind !== 'type')
123
+ .map((spec) => context.visit(spec.local)),
124
+ };
119
125
  },
120
126
 
121
127
  CallExpression(node, context) {
@@ -1168,6 +1174,8 @@ function join_template(items) {
1168
1174
  function normalize_child(node, normalized) {
1169
1175
  if (node.type === 'EmptyStatement') {
1170
1176
  return;
1177
+ } else if (node.type === 'Element' && node.id.type === 'Identifier' && node.id.name === 'style') {
1178
+ return;
1171
1179
  } else {
1172
1180
  normalized.push(node);
1173
1181
  }
@@ -1561,9 +1569,7 @@ export function transform(filename, source, analysis, to_ts) {
1561
1569
  to_ts,
1562
1570
  };
1563
1571
 
1564
- const program = /** @type {ESTree.Program} */ (
1565
- walk((analysis.ast), state, visitors)
1566
- );
1572
+ const program = /** @type {ESTree.Program} */ (walk(analysis.ast, state, visitors));
1567
1573
 
1568
1574
  for (const hoisted of state.hoisted) {
1569
1575
  program.body.unshift(hoisted);
@@ -40,6 +40,8 @@ export { flush_sync as flushSync, untrack, deferred } from './internal/client/ru
40
40
 
41
41
  export { RippleArray } from './array.js';
42
42
 
43
+ export { RippleSet } from './set.js';
44
+
43
45
  export { keyed } from './internal/client/for.js';
44
46
 
45
47
  export { user_effect as effect } from './internal/client/blocks.js';
@@ -0,0 +1,178 @@
1
+ import { get, increment, scope, set, tracked } from './internal/client/runtime.js';
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
+ ];
23
+
24
+ let init = false;
25
+
26
+ export class RippleSet extends Set {
27
+ #tracked_size;
28
+ #tracked_items = new Map();
29
+
30
+ constructor(iterable) {
31
+ super();
32
+
33
+ var block = scope();
34
+
35
+ if (iterable) {
36
+ for (var item of iterable) {
37
+ super.add(item);
38
+ this.#tracked_items.set(item, tracked(0, block));
39
+ }
40
+ }
41
+
42
+ this.#tracked_size = tracked(this.size, block);
43
+
44
+ if (!init) {
45
+ init = true;
46
+ this.#init();
47
+ }
48
+ }
49
+
50
+ #init() {
51
+ var proto = RippleSet.prototype;
52
+ var set_proto = Set.prototype;
53
+
54
+ for (const method of introspect_methods) {
55
+ if (!(method in set_proto)) {
56
+ continue;
57
+ }
58
+
59
+ proto[method] = function (...v) {
60
+ this.$size;
61
+
62
+ return set_proto[method].apply(this, v);
63
+ };
64
+ }
65
+
66
+ for (const method of compare_other_methods) {
67
+ if (!(method in set_proto)) {
68
+ continue;
69
+ }
70
+
71
+ proto[method] = function (other, ...v) {
72
+ this.$size;
73
+
74
+ if (other instanceof RippleSet) {
75
+ other.$size;
76
+ }
77
+
78
+ return set_proto[method].apply(this, [other, ...v]);
79
+ };
80
+ }
81
+
82
+ for (const method of new_other_methods) {
83
+ if (!(method in set_proto)) {
84
+ continue;
85
+ }
86
+
87
+ proto[method] = function (other, ...v) {
88
+ this.$size;
89
+
90
+ if (other instanceof RippleSet) {
91
+ other.$size;
92
+ }
93
+
94
+ return new RippleSet(
95
+ set_proto[method].apply(this, [other, ...v])
96
+ );
97
+ };
98
+ }
99
+ }
100
+
101
+ add(value) {
102
+ var block = scope();
103
+
104
+ if (!super.has(value)) {
105
+ super.add(value);
106
+ this.#tracked_items.set(value, tracked(0, block));
107
+ set(this.#tracked_size, this.size, block);
108
+ }
109
+
110
+ return this;
111
+ }
112
+
113
+ delete(value) {
114
+ var block = scope();
115
+
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
+ }
123
+
124
+ this.#tracked_items.delete(value);
125
+ set(this.#tracked_size, this.size, block);
126
+
127
+ return true;
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ has(value) {
134
+ var has = super.has(value);
135
+ var tracked_items = this.#tracked_items;
136
+ var t = tracked_items.get(value);
137
+
138
+ if (t === undefined) {
139
+ if (!has) {
140
+ // If the value doesn't exist, track the size in case it's added later
141
+ // but don't create tracked entries willy-nilly to track all possible values
142
+ this.$size;
143
+
144
+ return false;
145
+ }
146
+
147
+ t = tracked(0, block);
148
+ tracked_items.set(value, t);
149
+ }
150
+
151
+ get(t);
152
+ return has;
153
+ }
154
+
155
+ clear() {
156
+ var block = scope();
157
+
158
+ if (this.size > 0) {
159
+ for (var [value, t] of this.#tracked_items) {
160
+ increment(t, block);
161
+ }
162
+
163
+ super.clear();
164
+ this.#tracked_items.clear();
165
+ set(this.#tracked_size, 0, block);
166
+ }
167
+ }
168
+
169
+ get $size() {
170
+ return get(this.#tracked_size);
171
+ }
172
+
173
+ toJSON() {
174
+ this.$size;
175
+
176
+ return [...this];
177
+ }
178
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, RippleSet } from 'ripple';
3
+
4
+ describe('RippleSet', () => {
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 add and delete operations', () => {
24
+ component SetTest() {
25
+ let items = new RippleSet([1, 2, 3]);
26
+
27
+ <button onClick={() => items.add(4)}>{'add'}</button>
28
+ <button onClick={() => items.delete(2)}>{'delete'}</button>
29
+ <Child items={items} />
30
+ }
31
+
32
+ component Child({ items }) {
33
+ <pre>{JSON.stringify(items)}</pre>
34
+ <pre>{items.$size}</pre>
35
+ }
36
+
37
+ render(SetTest);
38
+
39
+ const addButton = container.querySelectorAll('button')[0];
40
+ const deleteButton = container.querySelectorAll('button')[1];
41
+
42
+ addButton.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
+ deleteButton.click();
49
+ flushSync();
50
+
51
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[1,3,4]');
52
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
53
+ });
54
+
55
+ it('handles clear operation', () => {
56
+ component SetTest() {
57
+ let items = new RippleSet([1, 2, 3]);
58
+
59
+ <button onClick={() => items.clear()}>{'clear'}</button>
60
+ <Child items={items} />
61
+ }
62
+
63
+ component Child({ items }) {
64
+ <pre>{JSON.stringify(items)}</pre>
65
+ <pre>{items.$size}</pre>
66
+ }
67
+
68
+ render(SetTest);
69
+
70
+ const clearButton = container.querySelector('button');
71
+
72
+ clearButton.click();
73
+ flushSync();
74
+
75
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
76
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
77
+ });
78
+
79
+ it('handles has operation', () => {
80
+ component SetTest() {
81
+ let items = new RippleSet([1, 2, 3]);
82
+ let $hasValue = items.has(2);
83
+
84
+ <button onClick={() => items.delete(2)}>{'delete'}</button>
85
+ <pre>{JSON.stringify($hasValue)}</pre>
86
+ }
87
+
88
+ render(SetTest);
89
+
90
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
91
+
92
+ const deleteButton = container.querySelectorAll('button')[0];
93
+
94
+ deleteButton.click();
95
+ flushSync();
96
+
97
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('false');
98
+ });
99
+ });
package/types/index.d.ts CHANGED
@@ -38,3 +38,15 @@ export type Context<T> = {
38
38
  };
39
39
 
40
40
  export declare function createContext<T>(initialValue: T): Context<T>;
41
+
42
+ export class RippleSet<T> extends Set<T> {
43
+ readonly $size: number;
44
+ isDisjointFrom(other: RippleSet<T> | Set<T>): boolean;
45
+ isSubsetOf(other: RippleSet<T> | Set<T>): boolean;
46
+ isSupersetOf(other: RippleSet<T> | Set<T>): boolean;
47
+ difference(other: RippleSet<T> | Set<T>): RippleSet<T>;
48
+ intersection(other: RippleSet<T> | Set<T>): RippleSet<T>;
49
+ symmetricDifference(other: RippleSet<T> | Set<T>): RippleSet<T>;
50
+ union(other: RippleSet<T> | Set<T>): RippleSet<T>;
51
+ toJSON(): T[];
52
+ }