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 +1 -1
- package/src/compiler/phases/1-parse/index.js +116 -14
- package/src/compiler/phases/3-transform/index.js +10 -4
- package/src/runtime/index.js +2 -0
- package/src/runtime/set.js +178 -0
- package/tests/set.test.ripple +99 -0
- package/types/index.d.ts +12 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/runtime/index.js
CHANGED
|
@@ -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
|
+
}
|