ripple 0.2.158 → 0.2.160
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 +2 -2
- package/src/compiler/phases/1-parse/index.js +75 -0
- package/src/compiler/phases/2-analyze/index.js +10 -9
- package/src/compiler/phases/3-transform/client/index.js +24 -54
- package/src/compiler/phases/3-transform/server/index.js +12 -0
- package/tests/client/compiler/compiler.basic.test.ripple +8 -8
- package/tests/client/composite/composite.dynamic-components.test.ripple +3 -1
- package/tests/client/computed-properties.test.ripple +18 -6
- package/tests/client/map.test.ripple +3 -3
- package/tests/client/set.test.ripple +3 -3
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.160",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
"typescript": "^5.9.2"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"ripple": "0.2.
|
|
84
|
+
"ripple": "0.2.160"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -636,6 +636,14 @@ function RipplePlugin(config) {
|
|
|
636
636
|
return this.finishNode(node, type);
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
// If we reach here, it means #Map or #Set is being called without 'new'
|
|
640
|
+
// Throw a TypeError to match JavaScript class constructor behavior
|
|
641
|
+
const constructorName = type === 'TrackedMapExpression' ? '#Map (TrackedMap)' : '#Set (TrackedSet)';
|
|
642
|
+
this.raise(
|
|
643
|
+
node.start,
|
|
644
|
+
`TypeError: Class constructor ${constructorName} cannot be invoked without 'new'`,
|
|
645
|
+
);
|
|
646
|
+
|
|
639
647
|
this.expect(tt.parenL); // expect '('
|
|
640
648
|
|
|
641
649
|
node.arguments = [];
|
|
@@ -2128,6 +2136,73 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
2128
2136
|
} else {
|
|
2129
2137
|
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
|
|
2130
2138
|
}
|
|
2139
|
+
} else if (hasBlankLine && onlyWhitespace && array_prop && parent[array_prop]) {
|
|
2140
|
+
// When there's a blank line between node and comment(s),
|
|
2141
|
+
// check if there's also a blank line after the comment(s) before the next node
|
|
2142
|
+
// If so, attach comments as trailing to preserve the grouping
|
|
2143
|
+
// Only do this for statement-level contexts (BlockStatement, Program),
|
|
2144
|
+
// not for Element children or other contexts
|
|
2145
|
+
const isStatementContext =
|
|
2146
|
+
parent.type === 'BlockStatement' || parent.type === 'Program';
|
|
2147
|
+
|
|
2148
|
+
// Don't apply for Component - let Prettier handle comment attachment there
|
|
2149
|
+
// Component bodies have different comment handling via metadata.elementLeadingComments
|
|
2150
|
+
if (!isStatementContext) {
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const currentIndex = parent[array_prop].indexOf(node);
|
|
2155
|
+
const nextSibling = parent[array_prop][currentIndex + 1];
|
|
2156
|
+
|
|
2157
|
+
if (nextSibling && nextSibling.loc) {
|
|
2158
|
+
// Find where the comment block ends
|
|
2159
|
+
let lastCommentIndex = 0;
|
|
2160
|
+
let lastCommentEnd = comments[0].end;
|
|
2161
|
+
|
|
2162
|
+
// Collect consecutive comments (without blank lines between them)
|
|
2163
|
+
while (comments[lastCommentIndex + 1]) {
|
|
2164
|
+
const currentComment = comments[lastCommentIndex];
|
|
2165
|
+
const nextComment = comments[lastCommentIndex + 1];
|
|
2166
|
+
const sliceBetween = source.slice(currentComment.end, nextComment.start);
|
|
2167
|
+
|
|
2168
|
+
// If there's a blank line, stop
|
|
2169
|
+
if (/\n\s*\n/.test(sliceBetween)) {
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
lastCommentIndex++;
|
|
2174
|
+
lastCommentEnd = nextComment.end;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Check if there's a blank line after the last comment and before next sibling
|
|
2178
|
+
const sliceAfterComments = source.slice(lastCommentEnd, nextSibling.start);
|
|
2179
|
+
const hasBlankLineAfter = /\n\s*\n/.test(sliceAfterComments);
|
|
2180
|
+
|
|
2181
|
+
if (hasBlankLineAfter) {
|
|
2182
|
+
// Don't attach comments as trailing if next sibling is an Element
|
|
2183
|
+
// and any comment falls within the Element's line range
|
|
2184
|
+
// This means the comments are inside the Element (between opening and closing tags)
|
|
2185
|
+
const nextIsElement = nextSibling.type === 'Element';
|
|
2186
|
+
const commentsInsideElement =
|
|
2187
|
+
nextIsElement &&
|
|
2188
|
+
nextSibling.loc &&
|
|
2189
|
+
comments.some((c) => {
|
|
2190
|
+
if (!c.loc) return false;
|
|
2191
|
+
// Check if comment is on a line between Element's start and end lines
|
|
2192
|
+
return (
|
|
2193
|
+
c.loc.start.line >= nextSibling.loc.start.line &&
|
|
2194
|
+
c.loc.end.line <= nextSibling.loc.end.line
|
|
2195
|
+
);
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
if (!commentsInsideElement) {
|
|
2199
|
+
// Attach all the comments as trailing
|
|
2200
|
+
for (let i = 0; i <= lastCommentIndex; i++) {
|
|
2201
|
+
(node.trailingComments ||= []).push(comments.shift());
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2131
2206
|
}
|
|
2132
2207
|
}
|
|
2133
2208
|
}
|
|
@@ -821,20 +821,21 @@ const visitors = {
|
|
|
821
821
|
* @param {any} context
|
|
822
822
|
*/
|
|
823
823
|
AwaitExpression(node, context) {
|
|
824
|
+
const parent_block = get_parent_block_node(context);
|
|
825
|
+
|
|
824
826
|
if (is_inside_component(context)) {
|
|
825
827
|
if (context.state.metadata?.await === false) {
|
|
826
828
|
context.state.metadata.await = true;
|
|
827
829
|
}
|
|
828
|
-
}
|
|
829
|
-
const parent_block = get_parent_block_node(context);
|
|
830
830
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
831
|
+
if (parent_block !== null && parent_block.type !== 'Component') {
|
|
832
|
+
if (context.state.inside_server_block === false) {
|
|
833
|
+
error(
|
|
834
|
+
'`await` is not allowed in client-side control-flow statements',
|
|
835
|
+
context.state.analysis.module.filename,
|
|
836
|
+
node,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
838
839
|
}
|
|
839
840
|
}
|
|
840
841
|
|
|
@@ -414,6 +414,30 @@ const visitors = {
|
|
|
414
414
|
context.state.metadata.tracking = true;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// Special handling for TrackedMapExpression and TrackedSetExpression
|
|
418
|
+
// When source is "new #Map(...)", the callee is TrackedMapExpression with empty arguments
|
|
419
|
+
// and the actual arguments are in NewExpression.arguments
|
|
420
|
+
if (callee.type === 'TrackedMapExpression' || callee.type === 'TrackedSetExpression') {
|
|
421
|
+
// Use NewExpression's arguments (the callee has empty arguments from parser)
|
|
422
|
+
const argsToUse = node.arguments.length > 0 ? node.arguments : callee.arguments;
|
|
423
|
+
|
|
424
|
+
if (context.state.to_ts) {
|
|
425
|
+
const className = callee.type === 'TrackedMapExpression' ? 'TrackedMap' : 'TrackedSet';
|
|
426
|
+
const alias = import_from_ripple_if_needed(className, context);
|
|
427
|
+
const calleeId = b.id(alias);
|
|
428
|
+
calleeId.loc = callee.loc;
|
|
429
|
+
calleeId.metadata = { tracked_shorthand: callee.type === 'TrackedMapExpression' ? '#Map' : '#Set' };
|
|
430
|
+
return b.new(calleeId, ...argsToUse.map((arg) => context.visit(arg)));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const helperName = callee.type === 'TrackedMapExpression' ? 'tracked_map' : 'tracked_set';
|
|
434
|
+
return b.call(
|
|
435
|
+
`_$_.${helperName}`,
|
|
436
|
+
b.id('__block'),
|
|
437
|
+
...argsToUse.map((arg) => context.visit(arg)),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
417
441
|
if (
|
|
418
442
|
context.state.to_ts ||
|
|
419
443
|
!is_inside_component(context, true) ||
|
|
@@ -424,22 +448,6 @@ const visitors = {
|
|
|
424
448
|
delete node.typeArguments;
|
|
425
449
|
}
|
|
426
450
|
|
|
427
|
-
// Special handling for TrackedMapExpression and TrackedSetExpression
|
|
428
|
-
// When source is "new #Map(...)", the callee is TrackedMapExpression with empty arguments
|
|
429
|
-
// and the actual arguments are in NewExpression.arguments
|
|
430
|
-
// We need to merge them before transforming
|
|
431
|
-
if (
|
|
432
|
-
context.state.to_ts &&
|
|
433
|
-
(callee.type === 'TrackedMapExpression' || callee.type === 'TrackedSetExpression')
|
|
434
|
-
) {
|
|
435
|
-
// If the callee has empty arguments, use the NewExpression's arguments instead
|
|
436
|
-
if (callee.arguments.length === 0 && node.arguments.length > 0) {
|
|
437
|
-
callee.arguments = node.arguments;
|
|
438
|
-
}
|
|
439
|
-
// Transform the tracked expression directly - it will return a NewExpression
|
|
440
|
-
return context.visit(callee);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
451
|
return context.next();
|
|
444
452
|
}
|
|
445
453
|
|
|
@@ -486,44 +494,6 @@ const visitors = {
|
|
|
486
494
|
);
|
|
487
495
|
},
|
|
488
496
|
|
|
489
|
-
TrackedMapExpression(node, context) {
|
|
490
|
-
if (context.state.to_ts) {
|
|
491
|
-
const mapAlias = import_from_ripple_if_needed('TrackedMap', context);
|
|
492
|
-
|
|
493
|
-
const calleeId = b.id(mapAlias);
|
|
494
|
-
// Preserve location from original node for Volar mapping
|
|
495
|
-
calleeId.loc = node.loc;
|
|
496
|
-
// Add metadata for Volar mapping - map "TrackedMap" identifier to "#Map" in source
|
|
497
|
-
calleeId.metadata = { tracked_shorthand: '#Map' };
|
|
498
|
-
return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return b.call(
|
|
502
|
-
'_$_.tracked_map',
|
|
503
|
-
b.id('__block'),
|
|
504
|
-
...node.arguments.map((arg) => context.visit(arg)),
|
|
505
|
-
);
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
TrackedSetExpression(node, context) {
|
|
509
|
-
if (context.state.to_ts) {
|
|
510
|
-
const setAlias = import_from_ripple_if_needed('TrackedSet', context);
|
|
511
|
-
|
|
512
|
-
const calleeId = b.id(setAlias);
|
|
513
|
-
// Preserve location from original node for Volar mapping
|
|
514
|
-
calleeId.loc = node.loc;
|
|
515
|
-
// Add metadata for Volar mapping - map "TrackedSet" identifier to "#Set" in source
|
|
516
|
-
calleeId.metadata = { tracked_shorthand: '#Set' };
|
|
517
|
-
return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return b.call(
|
|
521
|
-
'_$_.tracked_set',
|
|
522
|
-
b.id('__block'),
|
|
523
|
-
...node.arguments.map((arg) => context.visit(arg)),
|
|
524
|
-
);
|
|
525
|
-
},
|
|
526
|
-
|
|
527
497
|
TrackedExpression(node, context) {
|
|
528
498
|
return b.call('_$_.get', context.visit(node.argument));
|
|
529
499
|
},
|
|
@@ -151,6 +151,18 @@ const visitors = {
|
|
|
151
151
|
},
|
|
152
152
|
|
|
153
153
|
NewExpression(node, context) {
|
|
154
|
+
// Special handling for TrackedMapExpression and TrackedSetExpression
|
|
155
|
+
// When source is "new #Map(...)", the callee is TrackedMapExpression with empty arguments
|
|
156
|
+
// and the actual arguments are in NewExpression.arguments
|
|
157
|
+
const callee = node.callee;
|
|
158
|
+
if (callee.type === 'TrackedMapExpression' || callee.type === 'TrackedSetExpression') {
|
|
159
|
+
// Use NewExpression's arguments (the callee has empty arguments from parser)
|
|
160
|
+
const argsToUse = node.arguments.length > 0 ? node.arguments : callee.arguments;
|
|
161
|
+
// For SSR, use regular Map/Set
|
|
162
|
+
const constructorName = callee.type === 'TrackedMapExpression' ? 'Map' : 'Set';
|
|
163
|
+
return b.new(b.id(constructorName), ...argsToUse.map((arg) => context.visit(arg)));
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
if (!context.state.to_ts) {
|
|
155
167
|
delete node.typeArguments;
|
|
156
168
|
}
|
|
@@ -232,8 +232,8 @@ import {
|
|
|
232
232
|
component App() {
|
|
233
233
|
const items = #[1, 2, 3];
|
|
234
234
|
const obj = #{ a: 1, b: 2, c: 3 };
|
|
235
|
-
const set = #Set([1, 2, 3]);
|
|
236
|
-
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
235
|
+
const set = new #Set([1, 2, 3]);
|
|
236
|
+
const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
237
237
|
|
|
238
238
|
<div {ref () => {}} />
|
|
239
239
|
}
|
|
@@ -262,8 +262,8 @@ import {
|
|
|
262
262
|
component App() {
|
|
263
263
|
const items = #[1, 2, 3];
|
|
264
264
|
const obj = #{ a: 1, b: 2, c: 3 };
|
|
265
|
-
const set = #Set([1, 2, 3]);
|
|
266
|
-
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
265
|
+
const set = new #Set([1, 2, 3]);
|
|
266
|
+
const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
267
267
|
|
|
268
268
|
<div {ref () => {}} />
|
|
269
269
|
}
|
|
@@ -288,8 +288,8 @@ component App() {
|
|
|
288
288
|
component App() {
|
|
289
289
|
const items = #[1, 2, 3];
|
|
290
290
|
const obj = #{ a: 1, b: 2, c: 3 };
|
|
291
|
-
const set = #Set([1, 2, 3]);
|
|
292
|
-
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
291
|
+
const set = new #Set([1, 2, 3]);
|
|
292
|
+
const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
293
293
|
|
|
294
294
|
<div {ref () => {}} />
|
|
295
295
|
}
|
|
@@ -310,8 +310,8 @@ import { TrackedArray, TrackedMap, createRefKey as crk } from 'ripple';
|
|
|
310
310
|
component App() {
|
|
311
311
|
const items = #[1, 2, 3];
|
|
312
312
|
const obj = #{ a: 1, b: 2, c: 3 };
|
|
313
|
-
const set = #Set([1, 2, 3]);
|
|
314
|
-
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
313
|
+
const set = new #Set([1, 2, 3]);
|
|
314
|
+
const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
315
315
|
|
|
316
316
|
<div {ref () => {}} />
|
|
317
317
|
}
|
|
@@ -78,7 +78,9 @@ describe('composite > dynamic components', () => {
|
|
|
78
78
|
<@thing />
|
|
79
79
|
</div>
|
|
80
80
|
|
|
81
|
-
<button onClick={() => @thing = @thing === Child1 ? Child2 : Child1}>
|
|
81
|
+
<button onClick={() => (@thing = @thing === Child1 ? Child2 : Child1)}>
|
|
82
|
+
{'Change Child'}
|
|
83
|
+
</button>
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
render(App);
|
|
@@ -7,10 +7,16 @@ describe('computed tracked properties', () => {
|
|
|
7
7
|
[0]: track(0),
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<div>{obj.@[0]}</div>
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
<button
|
|
13
|
+
onClick={() => {
|
|
14
|
+
obj.@[0] += 1;
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
{'Increment'}
|
|
18
|
+
</button>
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
render(App);
|
|
16
22
|
expect(container).toMatchSnapshot();
|
|
@@ -28,10 +34,16 @@ describe('computed tracked properties', () => {
|
|
|
28
34
|
[0]: track(0),
|
|
29
35
|
};
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
<div>{obj.@[0]}</div>
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => {
|
|
41
|
+
obj.@[0]++;
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{'Increment'}
|
|
45
|
+
</button>
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
render(App);
|
|
37
49
|
expect(container).toMatchSnapshot();
|
|
@@ -122,7 +122,7 @@ describe('TrackedMap', () => {
|
|
|
122
122
|
|
|
123
123
|
it('creates empty TrackedMap using #Map() shorthand syntax', () => {
|
|
124
124
|
component MapTest() {
|
|
125
|
-
let map = #Map();
|
|
125
|
+
let map = new #Map();
|
|
126
126
|
|
|
127
127
|
<button onClick={() => map.set('a', 1)}>{'add'}</button>
|
|
128
128
|
<pre>{map.size}</pre>
|
|
@@ -141,7 +141,7 @@ describe('TrackedMap', () => {
|
|
|
141
141
|
|
|
142
142
|
it('creates TrackedMap with initial entries using #Map() shorthand syntax', () => {
|
|
143
143
|
component MapTest() {
|
|
144
|
-
let map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
144
|
+
let map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
145
145
|
let value = track(() => map.get('b'));
|
|
146
146
|
|
|
147
147
|
<button onClick={() => map.set('b', 10)}>{'update'}</button>
|
|
@@ -163,7 +163,7 @@ describe('TrackedMap', () => {
|
|
|
163
163
|
|
|
164
164
|
it('handles all operations with #Map() shorthand syntax', () => {
|
|
165
165
|
component MapTest() {
|
|
166
|
-
let map = #Map([['x', 100], ['y', 200]]);
|
|
166
|
+
let map = new #Map([['x', 100], ['y', 200]]);
|
|
167
167
|
let keys = track(() => Array.from(map.keys()));
|
|
168
168
|
|
|
169
169
|
<button onClick={() => map.set('z', 300)}>{'add'}</button>
|
|
@@ -80,7 +80,7 @@ describe('TrackedSet', () => {
|
|
|
80
80
|
|
|
81
81
|
it('creates empty TrackedSet using #Set() shorthand syntax', () => {
|
|
82
82
|
component SetTest() {
|
|
83
|
-
let items = #Set();
|
|
83
|
+
let items = new #Set();
|
|
84
84
|
|
|
85
85
|
<button onClick={() => items.add(1)}>{'add'}</button>
|
|
86
86
|
<pre>{items.size}</pre>
|
|
@@ -99,7 +99,7 @@ describe('TrackedSet', () => {
|
|
|
99
99
|
|
|
100
100
|
it('creates TrackedSet with initial values using #Set() shorthand syntax', () => {
|
|
101
101
|
component SetTest() {
|
|
102
|
-
let items = #Set([1, 2, 3, 4]);
|
|
102
|
+
let items = new #Set([1, 2, 3, 4]);
|
|
103
103
|
let hasValue = track(() => items.has(3));
|
|
104
104
|
|
|
105
105
|
<button onClick={() => items.delete(3)}>{'delete'}</button>
|
|
@@ -122,7 +122,7 @@ describe('TrackedSet', () => {
|
|
|
122
122
|
|
|
123
123
|
it('handles all operations with #Set() shorthand syntax', () => {
|
|
124
124
|
component SetTest() {
|
|
125
|
-
let items = #Set([10, 20, 30]);
|
|
125
|
+
let items = new #Set([10, 20, 30]);
|
|
126
126
|
let values = track(() => Array.from(items.values()));
|
|
127
127
|
|
|
128
128
|
<button onClick={() => items.add(40)}>{'add'}</button>
|