ripple 0.2.114 → 0.2.116

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.114",
6
+ "version": "0.2.116",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -167,6 +167,22 @@ function RipplePlugin(config) {
167
167
  }
168
168
  }
169
169
 
170
+ // Check if this is #Map or #Set
171
+ if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
172
+ const charAfter = this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
173
+ if (charAfter === 40) { // ( character
174
+ this.pos += 4; // consume '#Map'
175
+ return this.finishToken(tt.name, '#Map');
176
+ }
177
+ }
178
+ if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
179
+ const charAfter = this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
180
+ if (charAfter === 40) { // ( character
181
+ this.pos += 4; // consume '#Set'
182
+ return this.finishToken(tt.name, '#Set');
183
+ }
184
+ }
185
+
170
186
  // Check if this is #server
171
187
  if (this.input.slice(this.pos, this.pos + 7) === '#server') {
172
188
  // Check that next char after 'server' is whitespace, {, . (dot), or EOF
@@ -186,6 +202,27 @@ function RipplePlugin(config) {
186
202
  return this.finishToken(tt.name, '#server');
187
203
  }
188
204
  }
205
+
206
+ // Check if this is an invalid #Identifier pattern
207
+ // Valid patterns: #[, #{, #Map(, #Set(, #server
208
+ // If we see # followed by an uppercase letter that isn't Map or Set, it's an error
209
+ if (nextChar >= 65 && nextChar <= 90) { // A-Z
210
+ // Extract the identifier name
211
+ let identEnd = this.pos + 1;
212
+ while (identEnd < this.input.length) {
213
+ const ch = this.input.charCodeAt(identEnd);
214
+ if ((ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57) || ch === 95) {
215
+ // A-Z, a-z, 0-9, _
216
+ identEnd++;
217
+ } else {
218
+ break;
219
+ }
220
+ }
221
+ const identName = this.input.slice(this.pos + 1, identEnd);
222
+ if (identName !== 'Map' && identName !== 'Set') {
223
+ this.raise(this.pos, `Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`);
224
+ }
225
+ }
189
226
  }
190
227
  }
191
228
  if (code === 64) {
@@ -391,6 +428,12 @@ function RipplePlugin(config) {
391
428
  return this.finishNode(node, 'ServerIdentifier');
392
429
  }
393
430
 
431
+ // Check if this is #Map( or #Set(
432
+ if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
433
+ const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
434
+ return this.parseTrackedCollectionExpression(type);
435
+ }
436
+
394
437
  // Check if this is a tuple literal starting with #[
395
438
  if (this.type === tt.bracketL && this.value === '#[') {
396
439
  return this.parseTrackedArrayExpression();
@@ -449,6 +492,42 @@ function RipplePlugin(config) {
449
492
  return this.finishNode(node, 'ServerBlock');
450
493
  }
451
494
 
495
+ /**
496
+ * Parse `#Map(...)` or `#Set(...)` syntax for tracked collections
497
+ * Creates a TrackedMap or TrackedSet node with the arguments property
498
+ * @param {string} type - Either 'TrackedMap' or 'TrackedSet'
499
+ * @returns {any} TrackedMap or TrackedSet node
500
+ */
501
+ parseTrackedCollectionExpression(type) {
502
+ const node = this.startNode();
503
+ this.next(); // consume '#Map' or '#Set'
504
+ this.expect(tt.parenL); // expect '('
505
+
506
+ node.arguments = [];
507
+
508
+ // Parse arguments similar to function call arguments
509
+ let first = true;
510
+ while (!this.eat(tt.parenR)) {
511
+ if (!first) {
512
+ this.expect(tt.comma);
513
+ if (this.afterTrailingComma(tt.parenR)) break;
514
+ } else {
515
+ first = false;
516
+ }
517
+
518
+ if (this.type === tt.ellipsis) {
519
+ // Spread argument
520
+ const arg = this.parseSpread();
521
+ node.arguments.push(arg);
522
+ } else {
523
+ // Regular argument
524
+ node.arguments.push(this.parseMaybeAssign(false));
525
+ }
526
+ }
527
+
528
+ return this.finishNode(node, type);
529
+ }
530
+
452
531
  parseTrackedArrayExpression() {
453
532
  const node = this.startNode();
454
533
  this.next(); // consume the '#['
@@ -567,6 +567,23 @@ const visitors = {
567
567
 
568
568
  mark_control_flow_has_template(path);
569
569
 
570
+ // Store capitalized name for dynamic components/elements
571
+ if (node.id.tracked) {
572
+ const original_name = node.id.name;
573
+ const capitalized_name = original_name.charAt(0).toUpperCase() + original_name.slice(1);
574
+ node.metadata.ts_name = capitalized_name;
575
+ node.metadata.original_name = original_name;
576
+
577
+ // Mark the binding as a dynamic component so we can capitalize it everywhere
578
+ const binding = context.state.scope.get(original_name);
579
+ if (binding) {
580
+ if (!binding.metadata) {
581
+ binding.metadata = {};
582
+ }
583
+ binding.metadata.is_dynamic_component = true;
584
+ }
585
+ }
586
+
570
587
  if (is_dom_element) {
571
588
  if (node.id.name === 'head') {
572
589
  // head validation
@@ -161,6 +161,15 @@ const visitors = {
161
161
  if (is_reference(node, parent)) {
162
162
  if (context.state.to_ts) {
163
163
  if (node.tracked) {
164
+ // Check if this identifier is used as a dynamic component/element
165
+ // by checking if it has a capitalized name in metadata
166
+ const binding = context.state.scope.get(node.name);
167
+ if (binding?.metadata?.is_dynamic_component) {
168
+ // Capitalize the identifier for TypeScript
169
+ const capitalizedName = node.name.charAt(0).toUpperCase() + node.name.slice(1);
170
+ const capitalizedNode = { ...node, name: capitalizedName };
171
+ return b.member(capitalizedNode, b.literal('#v'), true);
172
+ }
164
173
  return b.member(node, b.literal('#v'), true);
165
174
  }
166
175
  } else {
@@ -341,11 +350,9 @@ const visitors = {
341
350
  context.state.imports.add(`import { TrackedArray } from 'ripple'`);
342
351
  }
343
352
 
344
- return b.new(
345
- b.call(
346
- b.member(b.id('TrackedArray'), b.id('from')),
347
- node.elements.map((el) => context.visit(el)),
348
- ),
353
+ return b.call(
354
+ b.member(b.id('TrackedArray'), b.id('from')),
355
+ ...node.elements.map((el) => context.visit(el)),
349
356
  );
350
357
  }
351
358
 
@@ -375,6 +382,48 @@ const visitors = {
375
382
  );
376
383
  },
377
384
 
385
+ TrackedMapExpression(node, context) {
386
+ if (context.state.to_ts) {
387
+ if (!context.state.imports.has(`import { TrackedMap } from 'ripple'`)) {
388
+ context.state.imports.add(`import { TrackedMap } from 'ripple'`);
389
+ }
390
+
391
+ const calleeId = b.id('TrackedMap');
392
+ // Preserve location from original node for Volar mapping
393
+ calleeId.loc = node.loc;
394
+ // Add metadata for Volar mapping - map "TrackedMap" identifier to "#Map" in source
395
+ calleeId.metadata = { tracked_shorthand: '#Map' };
396
+ return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
397
+ }
398
+
399
+ return b.call(
400
+ '_$_.tracked_map',
401
+ b.id('__block'),
402
+ ...node.arguments.map((arg) => context.visit(arg)),
403
+ );
404
+ },
405
+
406
+ TrackedSetExpression(node, context) {
407
+ if (context.state.to_ts) {
408
+ if (!context.state.imports.has(`import { TrackedSet } from 'ripple'`)) {
409
+ context.state.imports.add(`import { TrackedSet } from 'ripple'`);
410
+ }
411
+
412
+ const calleeId = b.id('TrackedSet');
413
+ // Preserve location from original node for Volar mapping
414
+ calleeId.loc = node.loc;
415
+ // Add metadata for Volar mapping - map "TrackedSet" identifier to "#Set" in source
416
+ calleeId.metadata = { tracked_shorthand: '#Set' };
417
+ return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
418
+ }
419
+
420
+ return b.call(
421
+ '_$_.tracked_set',
422
+ b.id('__block'),
423
+ ...node.arguments.map((arg) => context.visit(arg)),
424
+ );
425
+ },
426
+
378
427
  TrackedExpression(node, context) {
379
428
  return b.call('_$_.get', context.visit(node.argument));
380
429
  },
@@ -441,6 +490,22 @@ const visitors = {
441
490
  return context.next();
442
491
  },
443
492
 
493
+ VariableDeclarator(node, context) {
494
+ // In TypeScript mode, capitalize identifiers that are used as dynamic components
495
+ if (context.state.to_ts && node.id.type === 'Identifier') {
496
+ const binding = context.state.scope.get(node.id.name);
497
+ if (binding?.metadata?.is_dynamic_component) {
498
+ const capitalizedName = node.id.name.charAt(0).toUpperCase() + node.id.name.slice(1);
499
+ return {
500
+ ...node,
501
+ id: { ...node.id, name: capitalizedName },
502
+ init: node.init ? context.visit(node.init) : null,
503
+ };
504
+ }
505
+ }
506
+ return context.next();
507
+ },
508
+
444
509
  FunctionDeclaration(node, context) {
445
510
  return visit_function(node, context);
446
511
  },
@@ -1397,7 +1462,8 @@ function transform_ts_child(node, context) {
1397
1462
  // Do we need to do something special here?
1398
1463
  state.init.push(b.stmt(visit(node.expression, { ...state })));
1399
1464
  } else if (node.type === 'Element') {
1400
- const type = node.id.name;
1465
+ // Use capitalized name for dynamic components/elements in TypeScript output
1466
+ const type = node.metadata?.ts_name || node.id.name;
1401
1467
  const children = [];
1402
1468
  let has_children_props = false;
1403
1469
 
@@ -1462,33 +1528,50 @@ function transform_ts_child(node, context) {
1462
1528
  }
1463
1529
  }
1464
1530
 
1465
- let tracked_type;
1466
-
1467
- // Make VSCode happy about tracked components, as they're lowercase in the AST
1468
- // so VSCode doesn't bother tracking them as components, so this fixes that
1469
- if (node.id.tracked) {
1470
- tracked_type = state.scope.generate(type[0].toUpperCase() + type.slice(1));
1471
- state.init.push(
1472
- b.const(tracked_type, b.member(node.id, b.literal('#v'), true)),
1473
- );
1474
- }
1475
-
1476
- const opening_type = tracked_type
1477
- ? b.jsx_id(tracked_type)
1478
- : b.jsx_id(type);
1479
- opening_type.loc = node.id.loc;
1531
+ const opening_type = b.jsx_id(type);
1532
+ // Use node.id.loc if available, otherwise create a loc based on the element's position
1533
+ opening_type.loc = node.id.loc || {
1534
+ start: {
1535
+ line: node.loc.start.line,
1536
+ column: node.loc.start.column + 2, // After "<@"
1537
+ },
1538
+ end: {
1539
+ line: node.loc.start.line,
1540
+ column: node.loc.start.column + 2 + type.length,
1541
+ },
1542
+ };
1480
1543
 
1481
1544
  let closing_type = undefined;
1482
1545
 
1483
1546
  if (!node.selfClosing) {
1484
- closing_type = tracked_type
1485
- ? b.jsx_id(tracked_type)
1486
- : b.jsx_id(type);
1547
+ closing_type = b.jsx_id(type);
1548
+ closing_type.loc = {
1549
+ start: {
1550
+ line: node.loc.end.line,
1551
+ column: node.loc.end.column - type.length - 1,
1552
+ },
1553
+ end: {
1554
+ line: node.loc.end.line,
1555
+ column: node.loc.end.column - 1,
1556
+ },
1557
+ };
1487
1558
  }
1488
1559
 
1489
- state.init.push(
1490
- b.stmt(b.jsx_element(opening_type, attributes, children, node.selfClosing, closing_type)),
1560
+ const jsxElement = b.jsx_element(
1561
+ opening_type,
1562
+ attributes,
1563
+ children,
1564
+ node.selfClosing,
1565
+ closing_type,
1491
1566
  );
1567
+ // Preserve metadata from Element node for mapping purposes
1568
+ if (node.metadata && (node.metadata.ts_name || node.metadata.original_name)) {
1569
+ jsxElement.metadata = {
1570
+ ts_name: node.metadata.ts_name,
1571
+ original_name: node.metadata.original_name,
1572
+ };
1573
+ }
1574
+ state.init.push(b.stmt(jsxElement));
1492
1575
  } else if (node.type === 'IfStatement') {
1493
1576
  const consequent_scope = context.state.scopes.get(node.consequent);
1494
1577
  const consequent = b.block(
@@ -5,6 +5,14 @@ export const mapping_data = {
5
5
  completion: true,
6
6
  semantic: true,
7
7
  navigation: true,
8
+ rename: true,
9
+ codeActions: false, // set to false to disable auto import when importing yourself
10
+ formatting: false, // not doing formatting through Volar, using Prettier.
11
+ // these 3 below will be true by default
12
+ // leaving for reference
13
+ // hover: true,
14
+ // definition: true,
15
+ // references: true,
8
16
  };
9
17
 
10
18
  /**
@@ -22,6 +30,26 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
22
30
  let sourceIndex = 0;
23
31
  let generatedIndex = 0;
24
32
 
33
+ // Map to track capitalized names: original name -> capitalized name
34
+ /** @type {Map<string, string>} */
35
+ const capitalizedNames = new Map();
36
+ // Reverse map: capitalized name -> original name
37
+ /** @type {Map<string, string>} */
38
+ const reverseCapitalizedNames = new Map();
39
+
40
+ // Pre-walk to collect capitalized names from JSXElement nodes (transformed AST)
41
+ // These are identifiers that are used as dynamic components/elements
42
+ walk(ast, null, {
43
+ _(node, { next }) {
44
+ // Check JSXElement nodes with metadata (preserved from Element nodes)
45
+ if (node.type === 'JSXElement' && node.metadata?.ts_name && node.metadata?.original_name) {
46
+ capitalizedNames.set(node.metadata.original_name, node.metadata.ts_name);
47
+ reverseCapitalizedNames.set(node.metadata.ts_name, node.metadata.original_name);
48
+ }
49
+ next();
50
+ }
51
+ });
52
+
25
53
  /**
26
54
  * Check if character is a word boundary (not alphanumeric or underscore)
27
55
  * @param {string} char
@@ -31,6 +59,29 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
31
59
  return char === undefined || !/[a-zA-Z0-9_$]/.test(char);
32
60
  };
33
61
 
62
+ /**
63
+ * Check if a position is inside a comment
64
+ * @param {number} pos - Position to check
65
+ * @returns {boolean}
66
+ */
67
+ const isInComment = (pos) => {
68
+ // Check for single-line comment: find start of line and check if there's // before this position
69
+ let lineStart = source.lastIndexOf('\n', pos - 1) + 1;
70
+ const lineBeforePos = source.substring(lineStart, pos);
71
+ if (lineBeforePos.includes('//')) {
72
+ return true;
73
+ }
74
+ // Check for multi-line comment: look backwards for /* and forwards for */
75
+ const lastCommentStart = source.lastIndexOf('/*', pos);
76
+ if (lastCommentStart !== -1) {
77
+ const commentEnd = source.indexOf('*/', lastCommentStart);
78
+ if (commentEnd === -1 || commentEnd > pos) {
79
+ return true; // We're inside an unclosed or open comment
80
+ }
81
+ }
82
+ return false;
83
+ };
84
+
34
85
  /**
35
86
  * Find text in source string, searching character by character from sourceIndex
36
87
  * @param {string} text - Text to find
@@ -46,6 +97,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
46
97
  }
47
98
  }
48
99
  if (match) {
100
+ // Skip if this match is inside a comment
101
+ if (isInComment(i)) {
102
+ continue;
103
+ }
104
+
49
105
  // Check word boundaries for identifier-like tokens
50
106
  const isIdentifierLike = /^[a-zA-Z_$]/.test(text);
51
107
  if (isIdentifierLike) {
@@ -96,23 +152,71 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
96
152
  };
97
153
 
98
154
  // Collect text tokens from AST nodes
99
- /** @type {string[]} */
155
+ // Tokens can be either strings or objects with source/generated properties
156
+ /** @type {Array<string | {source: string, generated: string}>} */
100
157
  const tokens = [];
101
158
 
159
+ // Collect import declarations for full-statement mappings
160
+ /** @type {Array<{start: number, end: number}>} */
161
+ const importDeclarations = [];
162
+
102
163
  // We have to visit everything in generated order to maintain correct indices
103
164
  walk(ast, null, {
104
165
  _(node, { visit }) {
105
166
  // Collect key node types: Identifiers, Literals, and JSX Elements
167
+ // Only collect tokens from nodes with .loc (skip synthesized nodes like children attribute)
106
168
  if (node.type === 'Identifier' && node.name) {
107
- tokens.push(node.name);
169
+ if (node.loc) {
170
+ // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
171
+ if (node.metadata?.tracked_shorthand) {
172
+ tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name });
173
+ } else {
174
+ // Check if this identifier was capitalized (reverse lookup)
175
+ const originalName = reverseCapitalizedNames.get(node.name);
176
+ if (originalName) {
177
+ // This is a capitalized name in generated code, map to lowercase in source
178
+ tokens.push({ source: originalName, generated: node.name });
179
+ } else {
180
+ // Check if this identifier should be capitalized (forward lookup)
181
+ const capitalizedName = capitalizedNames.get(node.name);
182
+ if (capitalizedName) {
183
+ tokens.push({ source: node.name, generated: capitalizedName });
184
+ } else {
185
+ tokens.push(node.name);
186
+ }
187
+ }
188
+ }
189
+ }
108
190
  return; // Leaf node, don't traverse further
109
191
  } else if (node.type === 'JSXIdentifier' && node.name) {
110
- tokens.push(node.name);
192
+ if (node.loc) {
193
+ // Check if this was capitalized (reverse lookup)
194
+ const originalName = reverseCapitalizedNames.get(node.name);
195
+ if (originalName) {
196
+ tokens.push({ source: originalName, generated: node.name });
197
+ } else {
198
+ // Check if this should be capitalized (forward lookup)
199
+ const capitalizedName = capitalizedNames.get(node.name);
200
+ if (capitalizedName) {
201
+ tokens.push({ source: node.name, generated: capitalizedName });
202
+ } else {
203
+ tokens.push(node.name);
204
+ }
205
+ }
206
+ }
111
207
  return; // Leaf node, don't traverse further
112
208
  } else if (node.type === 'Literal' && node.raw) {
113
- tokens.push(node.raw);
209
+ if (node.loc) {
210
+ tokens.push(node.raw);
211
+ }
114
212
  return; // Leaf node, don't traverse further
115
213
  } else if (node.type === 'ImportDeclaration') {
214
+ // Collect import declaration range for full-statement mapping
215
+ // TypeScript reports unused imports with diagnostics covering the entire statement
216
+ if (node.start !== undefined && node.end !== undefined) {
217
+ importDeclarations.push({ start: node.start, end: node.end });
218
+ }
219
+
116
220
  // Visit specifiers in source order
117
221
  if (node.specifiers) {
118
222
  for (const specifier of node.specifiers) {
@@ -209,7 +313,20 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
209
313
 
210
314
  // 3. Push closing tag name (not visited by AST walker)
211
315
  if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
212
- tokens.push(node.closingElement.name.name);
316
+ const closingName = node.closingElement.name.name;
317
+ // Check if this was capitalized (reverse lookup)
318
+ const originalName = reverseCapitalizedNames.get(closingName);
319
+ if (originalName) {
320
+ tokens.push({ source: originalName, generated: closingName });
321
+ } else {
322
+ // Check if this should be capitalized (forward lookup)
323
+ const capitalizedName = capitalizedNames.get(closingName);
324
+ if (capitalizedName) {
325
+ tokens.push({ source: closingName, generated: capitalizedName });
326
+ } else {
327
+ tokens.push(closingName);
328
+ }
329
+ }
213
330
  }
214
331
 
215
332
  return;
@@ -988,23 +1105,65 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
988
1105
  });
989
1106
 
990
1107
  // Process each token in order
991
- for (const text of tokens) {
992
- const sourcePos = findInSource(text);
993
- const genPos = findInGenerated(text);
1108
+ for (const token of tokens) {
1109
+ let sourceText, generatedText;
1110
+
1111
+ if (typeof token === 'string') {
1112
+ sourceText = token;
1113
+ generatedText = token;
1114
+ } else {
1115
+ // Token with different source and generated names
1116
+ sourceText = token.source;
1117
+ generatedText = token.generated;
1118
+ }
1119
+
1120
+ const sourcePos = findInSource(sourceText);
1121
+ const genPos = findInGenerated(generatedText);
994
1122
 
995
1123
  if (sourcePos !== null && genPos !== null) {
996
1124
  mappings.push({
997
1125
  sourceOffsets: [sourcePos],
998
1126
  generatedOffsets: [genPos],
999
- lengths: [text.length],
1127
+ lengths: [sourceText.length],
1000
1128
  data: mapping_data,
1001
1129
  });
1002
1130
  }
1003
1131
  }
1004
1132
 
1133
+ // Add full-statement mappings for import declarations
1134
+ // TypeScript reports unused import diagnostics covering the entire import statement
1135
+ // Use verification-only mapping to avoid duplicate hover/completion
1136
+ for (const importDecl of importDeclarations) {
1137
+ const length = importDecl.end - importDecl.start;
1138
+ mappings.push({
1139
+ sourceOffsets: [importDecl.start],
1140
+ generatedOffsets: [importDecl.start], // Same position in generated code
1141
+ lengths: [length],
1142
+ data: {
1143
+ // only verification (diagnostics) to avoid duplicate hover/completion
1144
+ verification: true
1145
+ },
1146
+ });
1147
+ }
1148
+
1005
1149
  // Sort mappings by source offset
1006
1150
  mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
1007
1151
 
1152
+ // Add a mapping for the very beginning of the file to handle import additions
1153
+ // This ensures that code actions adding imports at the top work correctly
1154
+ if (mappings.length > 0 && mappings[0].sourceOffsets[0] > 0) {
1155
+ mappings.unshift({
1156
+ sourceOffsets: [0],
1157
+ generatedOffsets: [0],
1158
+ lengths: [1],
1159
+ data: {
1160
+ ...mapping_data,
1161
+ codeActions: true, // auto-import
1162
+ rename: false, // avoid rename for a “dummy” mapping
1163
+ }
1164
+ });
1165
+ }
1166
+
1008
1167
  return {
1009
1168
  code: generated_code,
1010
1169
  mappings,
@@ -78,6 +78,22 @@ export interface TrackedObjectExpression extends Omit<ObjectExpression, 'type'>
78
78
  properties: (Property | SpreadElement)[];
79
79
  }
80
80
 
81
+ /**
82
+ * Tracked Map expression node
83
+ */
84
+ export interface TrackedMapExpression extends Omit<Node, 'type'> {
85
+ type: 'TrackedMapExpression';
86
+ arguments: (Expression | SpreadElement)[];
87
+ }
88
+
89
+ /**
90
+ * Tracked Set expression node
91
+ */
92
+ export interface TrackedSetExpression extends Omit<Node, 'type'> {
93
+ type: 'TrackedSetExpression';
94
+ arguments: (Expression | SpreadElement)[];
95
+ }
96
+
81
97
  /**
82
98
  * Ripple component node
83
99
  */