ripple 0.2.159 → 0.2.161

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 an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.159",
6
+ "version": "0.2.161",
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.159"
84
+ "ripple": "0.2.161"
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
  }
@@ -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}>{'Change Child'}</button>
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
- <div>{obj.@[0]}</div>
10
+ <div>{obj.@[0]}</div>
11
11
 
12
- <button onClick={() => { obj.@[0] += 1 }}>{"Increment"}</button>
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
- <div>{obj.@[0]}</div>
37
+ <div>{obj.@[0]}</div>
32
38
 
33
- <button onClick={() => { obj.@[0]++ }}>{"Increment"}</button>
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>