ripple 0.2.115 → 0.2.118

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.
Files changed (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
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.115",
6
+ "version": "0.2.118",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -63,21 +63,21 @@
63
63
  "#public": "./types/index.d.ts"
64
64
  },
65
65
  "dependencies": {
66
- "@jridgewell/sourcemap-codec": "^1.5.5",
67
- "@sveltejs/acorn-typescript": "^1.0.6",
68
- "acorn": "^8.15.0",
69
- "clsx": "^2.1.1",
70
- "devalue": "^5.3.2",
71
- "esm-env": "^1.2.2",
72
- "esrap": "^2.1.0",
73
- "is-reference": "^3.0.3",
74
- "magic-string": "^0.30.18",
75
- "muggle-string": "^0.4.1",
76
- "zimmerframe": "^1.1.2"
66
+ "@jridgewell/sourcemap-codec": "catalog:default",
67
+ "@sveltejs/acorn-typescript": "catalog:default",
68
+ "acorn": "catalog:default",
69
+ "clsx": "catalog:default",
70
+ "devalue": "catalog:default",
71
+ "esm-env": "catalog:default",
72
+ "esrap": "catalog:default",
73
+ "is-reference": "catalog:default",
74
+ "magic-string": "catalog:default",
75
+ "muggle-string": "catalog:default",
76
+ "zimmerframe": "catalog:default"
77
77
  },
78
78
  "devDependencies": {
79
- "@types/estree": "^1.0.8",
80
- "@types/node": "^24.3.0",
81
- "typescript": "^5.9.2"
79
+ "@types/estree": "catalog:default",
80
+ "@types/node": "catalog:default",
81
+ "typescript": "catalog:default"
82
82
  }
83
- }
83
+ }
@@ -41,9 +41,28 @@ export function compile(source, filename, options = {}) {
41
41
  export function compile_to_volar_mappings(source, filename) {
42
42
  // Parse and transform
43
43
  const ast = parse_module(source);
44
+
45
+ // Add unique IDs to import declarations before transformation
46
+ // This allows us to match source imports with generated imports reliably
47
+ // This strategy can potentially be used for other node types in the future
48
+ let gen_id = 0;
49
+ const source_import_map = new Map();
50
+ for (const node of ast.body) {
51
+ if (node.type === 'ImportDeclaration') {
52
+ const start = /** @type {any} */ (node).start;
53
+ const end = /** @type {any} */ (node).end;
54
+ if (start !== undefined && end !== undefined) {
55
+ // Add a unique ID as a string property that will be copied during transformation
56
+ const id = `__volar_import_${gen_id++}__`;
57
+ /** @type {any} */ (node).__volar_id = id;
58
+ source_import_map.set(id, { start, end });
59
+ }
60
+ }
61
+ }
62
+
44
63
  const analysis = analyze(ast, filename);
45
64
  const transformed = transform_client(filename, source, analysis, true);
46
65
 
47
66
  // Create volar mappings directly from the AST instead of relying on esrap's sourcemap
48
- return convert_source_map_to_mappings(transformed.ast, source, transformed.js.code);
67
+ return convert_source_map_to_mappings(transformed.ast, source, transformed.js.code, source_import_map);
49
68
  }
@@ -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 '#['
@@ -350,11 +350,9 @@ const visitors = {
350
350
  context.state.imports.add(`import { TrackedArray } from 'ripple'`);
351
351
  }
352
352
 
353
- return b.new(
354
- b.call(
355
- b.member(b.id('TrackedArray'), b.id('from')),
356
- node.elements.map((el) => context.visit(el)),
357
- ),
353
+ return b.call(
354
+ b.member(b.id('TrackedArray'), b.id('from')),
355
+ ...node.elements.map((el) => context.visit(el)),
358
356
  );
359
357
  }
360
358
 
@@ -384,6 +382,48 @@ const visitors = {
384
382
  );
385
383
  },
386
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
+
387
427
  TrackedExpression(node, context) {
388
428
  return b.call('_$_.get', context.visit(node.argument));
389
429
  },
@@ -459,7 +499,7 @@ const visitors = {
459
499
  return {
460
500
  ...node,
461
501
  id: { ...node.id, name: capitalizedName },
462
- init: node.init ? context.visit(node.init) : null
502
+ init: node.init ? context.visit(node.init) : null,
463
503
  };
464
504
  }
465
505
  }
@@ -1517,12 +1557,18 @@ function transform_ts_child(node, context) {
1517
1557
  };
1518
1558
  }
1519
1559
 
1520
- const jsxElement = 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,
1566
+ );
1521
1567
  // Preserve metadata from Element node for mapping purposes
1522
1568
  if (node.metadata && (node.metadata.ts_name || node.metadata.original_name)) {
1523
1569
  jsxElement.metadata = {
1524
1570
  ts_name: node.metadata.ts_name,
1525
- original_name: node.metadata.original_name
1571
+ original_name: node.metadata.original_name,
1526
1572
  };
1527
1573
  }
1528
1574
  state.init.push(b.stmt(jsxElement));
@@ -20,22 +20,23 @@ export const mapping_data = {
20
20
  * @param {any} ast - The transformed AST
21
21
  * @param {string} source - Original source code
22
22
  * @param {string} generated_code - Generated code from esrap
23
+ * @param {Map<string, {start: number, end: number}>} [source_import_map] - Map of __volar_id strings to source positions
23
24
  * @returns {object}
24
25
  */
25
- export function convert_source_map_to_mappings(ast, source, generated_code) {
26
+ export function convert_source_map_to_mappings(ast, source, generated_code, source_import_map) {
26
27
  /** @type {Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}>} */
27
28
  const mappings = [];
28
29
 
29
30
  // Maintain indices that walk through source and generated code
30
- let sourceIndex = 0;
31
- let generatedIndex = 0;
31
+ let source_index = 0;
32
+ let generated_index = 0;
32
33
 
33
34
  // Map to track capitalized names: original name -> capitalized name
34
35
  /** @type {Map<string, string>} */
35
- const capitalizedNames = new Map();
36
+ const capitalized_names = new Map();
36
37
  // Reverse map: capitalized name -> original name
37
38
  /** @type {Map<string, string>} */
38
- const reverseCapitalizedNames = new Map();
39
+ const reverse_capitalized_names = new Map();
39
40
 
40
41
  // Pre-walk to collect capitalized names from JSXElement nodes (transformed AST)
41
42
  // These are identifiers that are used as dynamic components/elements
@@ -43,8 +44,8 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
43
44
  _(node, { next }) {
44
45
  // Check JSXElement nodes with metadata (preserved from Element nodes)
45
46
  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);
47
+ capitalized_names.set(node.metadata.original_name, node.metadata.ts_name);
48
+ reverse_capitalized_names.set(node.metadata.ts_name, node.metadata.original_name);
48
49
  }
49
50
  next();
50
51
  }
@@ -55,7 +56,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
55
56
  * @param {string} char
56
57
  * @returns {boolean}
57
58
  */
58
- const isWordBoundary = (char) => {
59
+ const is_word_boundary = (char) => {
59
60
  return char === undefined || !/[a-zA-Z0-9_$]/.test(char);
60
61
  };
61
62
 
@@ -64,7 +65,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
64
65
  * @param {number} pos - Position to check
65
66
  * @returns {boolean}
66
67
  */
67
- const isInComment = (pos) => {
68
+ const is_in_comment = (pos) => {
68
69
  // Check for single-line comment: find start of line and check if there's // before this position
69
70
  let lineStart = source.lastIndexOf('\n', pos - 1) + 1;
70
71
  const lineBeforePos = source.substring(lineStart, pos);
@@ -87,8 +88,8 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
87
88
  * @param {string} text - Text to find
88
89
  * @returns {number|null} - Source position or null
89
90
  */
90
- const findInSource = (text) => {
91
- for (let i = sourceIndex; i <= source.length - text.length; i++) {
91
+ const find_in_source = (text) => {
92
+ for (let i = source_index; i <= source.length - text.length; i++) {
92
93
  let match = true;
93
94
  for (let j = 0; j < text.length; j++) {
94
95
  if (source[i + j] !== text[j]) {
@@ -98,7 +99,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
98
99
  }
99
100
  if (match) {
100
101
  // Skip if this match is inside a comment
101
- if (isInComment(i)) {
102
+ if (is_in_comment(i)) {
102
103
  continue;
103
104
  }
104
105
 
@@ -107,12 +108,12 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
107
108
  if (isIdentifierLike) {
108
109
  const charBefore = source[i - 1];
109
110
  const charAfter = source[i + text.length];
110
- if (!isWordBoundary(charBefore) || !isWordBoundary(charAfter)) {
111
+ if (!is_word_boundary(charBefore) || !is_word_boundary(charAfter)) {
111
112
  continue; // Not a whole word match, keep searching
112
113
  }
113
114
  }
114
115
 
115
- sourceIndex = i + text.length;
116
+ source_index = i + text.length;
116
117
  return i;
117
118
  }
118
119
  }
@@ -120,12 +121,12 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
120
121
  };
121
122
 
122
123
  /**
123
- * Find text in generated code, searching character by character from generatedIndex
124
+ * Find text in generated code, searching character by character from generated_index
124
125
  * @param {string} text - Text to find
125
126
  * @returns {number|null} - Generated position or null
126
127
  */
127
- const findInGenerated = (text) => {
128
- for (let i = generatedIndex; i <= generated_code.length - text.length; i++) {
128
+ const find_in_generated = (text) => {
129
+ for (let i = generated_index; i <= generated_code.length - text.length; i++) {
129
130
  let match = true;
130
131
  for (let j = 0; j < text.length; j++) {
131
132
  if (generated_code[i + j] !== text[j]) {
@@ -139,12 +140,12 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
139
140
  if (isIdentifierLike) {
140
141
  const charBefore = generated_code[i - 1];
141
142
  const charAfter = generated_code[i + text.length];
142
- if (!isWordBoundary(charBefore) || !isWordBoundary(charAfter)) {
143
+ if (!is_word_boundary(charBefore) || !is_word_boundary(charAfter)) {
143
144
  continue; // Not a whole word match, keep searching
144
145
  }
145
146
  }
146
147
 
147
- generatedIndex = i + text.length;
148
+ generated_index = i + text.length;
148
149
  return i;
149
150
  }
150
151
  }
@@ -157,8 +158,8 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
157
158
  const tokens = [];
158
159
 
159
160
  // Collect import declarations for full-statement mappings
160
- /** @type {Array<{start: number, end: number}>} */
161
- const importDeclarations = [];
161
+ /** @type {Array<{id: string, node: any}>} */
162
+ const import_declarations = [];
162
163
 
163
164
  // We have to visit everything in generated order to maintain correct indices
164
165
  walk(ast, null, {
@@ -167,18 +168,29 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
167
168
  // Only collect tokens from nodes with .loc (skip synthesized nodes like children attribute)
168
169
  if (node.type === 'Identifier' && node.name) {
169
170
  if (node.loc) {
170
- // Check if this identifier was capitalized (reverse lookup)
171
- const originalName = reverseCapitalizedNames.get(node.name);
172
- if (originalName) {
173
- // This is a capitalized name in generated code, map to lowercase in source
174
- tokens.push({ source: originalName, generated: node.name });
171
+ // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
172
+ if (node.metadata?.tracked_shorthand) {
173
+ tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name });
175
174
  } else {
176
- // Check if this identifier should be capitalized (forward lookup)
177
- const capitalizedName = capitalizedNames.get(node.name);
178
- if (capitalizedName) {
179
- tokens.push({ source: node.name, generated: capitalizedName });
175
+ // Check if this identifier was capitalized (reverse lookup)
176
+ const original_name = reverse_capitalized_names.get(node.name);
177
+ if (original_name) {
178
+ // This is a capitalized name in generated code, map to lowercase in source
179
+ tokens.push({ source: original_name, generated: node.name });
180
180
  } else {
181
- tokens.push(node.name);
181
+ // Check if this identifier should be capitalized (forward lookup)
182
+ const cap_name = capitalized_names.get(node.name);
183
+ if (cap_name) {
184
+ tokens.push({ source: node.name, generated: cap_name });
185
+ } else {
186
+ // Check if this identifier should be capitalized (forward lookup)
187
+ const cap_name = capitalized_names.get(node.name);
188
+ if (cap_name) {
189
+ tokens.push({ source: node.name, generated: cap_name });
190
+ } else {
191
+ tokens.push(node.name);
192
+ }
193
+ }
182
194
  }
183
195
  }
184
196
  }
@@ -186,12 +198,12 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
186
198
  } else if (node.type === 'JSXIdentifier' && node.name) {
187
199
  if (node.loc) {
188
200
  // Check if this was capitalized (reverse lookup)
189
- const originalName = reverseCapitalizedNames.get(node.name);
201
+ const originalName = reverse_capitalized_names.get(node.name);
190
202
  if (originalName) {
191
203
  tokens.push({ source: originalName, generated: node.name });
192
204
  } else {
193
205
  // Check if this should be capitalized (forward lookup)
194
- const capitalizedName = capitalizedNames.get(node.name);
206
+ const capitalizedName = capitalized_names.get(node.name);
195
207
  if (capitalizedName) {
196
208
  tokens.push({ source: node.name, generated: capitalizedName });
197
209
  } else {
@@ -206,10 +218,16 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
206
218
  }
207
219
  return; // Leaf node, don't traverse further
208
220
  } else if (node.type === 'ImportDeclaration') {
209
- // Collect import declaration range for full-statement mapping
221
+ // Collect import declaration for full-statement mapping
210
222
  // TypeScript reports unused imports with diagnostics covering the entire statement
211
- if (node.start !== undefined && node.end !== undefined) {
212
- importDeclarations.push({ start: node.start, end: node.end });
223
+ // Store the __volar_id - we'll find the generated position later by searching
224
+ const volar_id = /** @type {any} */ (node).__volar_id;
225
+ if (volar_id) {
226
+ import_declarations.push({
227
+ id: volar_id,
228
+ // We'll calculate genStart/genEnd later by searching in generated code
229
+ node: node
230
+ });
213
231
  }
214
232
 
215
233
  // Visit specifiers in source order
@@ -310,12 +328,12 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
310
328
  if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
311
329
  const closingName = node.closingElement.name.name;
312
330
  // Check if this was capitalized (reverse lookup)
313
- const originalName = reverseCapitalizedNames.get(closingName);
331
+ const originalName = reverse_capitalized_names.get(closingName);
314
332
  if (originalName) {
315
333
  tokens.push({ source: originalName, generated: closingName });
316
334
  } else {
317
335
  // Check if this should be capitalized (forward lookup)
318
- const capitalizedName = capitalizedNames.get(closingName);
336
+ const capitalizedName = capitalized_names.get(closingName);
319
337
  if (capitalizedName) {
320
338
  tokens.push({ source: closingName, generated: capitalizedName });
321
339
  } else {
@@ -1101,25 +1119,25 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
1101
1119
 
1102
1120
  // Process each token in order
1103
1121
  for (const token of tokens) {
1104
- let sourceText, generatedText;
1122
+ let source_text, generated_text;
1105
1123
 
1106
1124
  if (typeof token === 'string') {
1107
- sourceText = token;
1108
- generatedText = token;
1125
+ source_text = token;
1126
+ generated_text = token;
1109
1127
  } else {
1110
1128
  // Token with different source and generated names
1111
- sourceText = token.source;
1112
- generatedText = token.generated;
1129
+ source_text = token.source;
1130
+ generated_text = token.generated;
1113
1131
  }
1114
1132
 
1115
- const sourcePos = findInSource(sourceText);
1116
- const genPos = findInGenerated(generatedText);
1133
+ const source_pos = find_in_source(source_text);
1134
+ const gen_pos = find_in_generated(generated_text);
1117
1135
 
1118
- if (sourcePos !== null && genPos !== null) {
1136
+ if (source_pos !== null && gen_pos !== null) {
1119
1137
  mappings.push({
1120
- sourceOffsets: [sourcePos],
1121
- generatedOffsets: [genPos],
1122
- lengths: [sourceText.length],
1138
+ sourceOffsets: [source_pos],
1139
+ generatedOffsets: [gen_pos],
1140
+ lengths: [source_text.length],
1123
1141
  data: mapping_data,
1124
1142
  });
1125
1143
  }
@@ -1128,17 +1146,46 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
1128
1146
  // Add full-statement mappings for import declarations
1129
1147
  // TypeScript reports unused import diagnostics covering the entire import statement
1130
1148
  // Use verification-only mapping to avoid duplicate hover/completion
1131
- for (const importDecl of importDeclarations) {
1132
- const length = importDecl.end - importDecl.start;
1133
- mappings.push({
1134
- sourceOffsets: [importDecl.start],
1135
- generatedOffsets: [importDecl.start], // Same position in generated code
1136
- lengths: [length],
1137
- data: {
1138
- // only verification (diagnostics) to avoid duplicate hover/completion
1139
- verification: true
1140
- },
1141
- });
1149
+
1150
+ // Use the source import map from the original AST (before transformation)
1151
+ // The __volar_id property is preserved through transformation via object spread
1152
+ if (source_import_map && import_declarations.length > 0) {
1153
+ // We need to find where each import appears in the generated code
1154
+ // Search for "import" keywords and match them to our collected imports
1155
+ let gen_search_index = 0;
1156
+
1157
+ for (const import_decl of import_declarations) {
1158
+ // Look up the source position using the __volar_id
1159
+ const source_range = source_import_map.get(import_decl.id);
1160
+ if (!source_range) continue; // Skip if we don't have source info for this ID
1161
+
1162
+ // Find this import statement in the generated code
1163
+ // Search for "import " starting from our last position
1164
+ const import_keyword_index = generated_code.indexOf('import ', gen_search_index);
1165
+ if (import_keyword_index === -1) continue; // Couldn't find it
1166
+
1167
+ // Find the semicolon or end of line for this import
1168
+ let gen_end = generated_code.indexOf(';', import_keyword_index);
1169
+ if (gen_end === -1) gen_end = generated_code.indexOf('\n', import_keyword_index);
1170
+ if (gen_end === -1) gen_end = generated_code.length;
1171
+ else gen_end += 1; // Include the semicolon
1172
+
1173
+ const get_start = import_keyword_index;
1174
+ gen_search_index = gen_end; // Next search starts after this import
1175
+
1176
+ const source_length = source_range.end - source_range.start;
1177
+ const get_length = gen_end - get_start;
1178
+
1179
+ mappings.push({
1180
+ sourceOffsets: [source_range.start],
1181
+ generatedOffsets: [get_start],
1182
+ lengths: [Math.min(source_length, get_length)],
1183
+ data: {
1184
+ // only verification (diagnostics) to avoid duplicate hover/completion
1185
+ verification: true
1186
+ },
1187
+ });
1188
+ }
1142
1189
  }
1143
1190
 
1144
1191
  // Sort mappings by source offset
@@ -525,18 +525,28 @@ const visitors = {
525
525
  return;
526
526
  }
527
527
 
528
- // TODO: alternative (else if / else)
529
- context.state.init.push(
530
- b.if(
531
- context.visit(node.test),
532
- b.block(
533
- transform_body(node.consequent.body, {
534
- ...context,
535
- state: { ...context.state, scope: context.state.scopes.get(node.consequent) },
536
- }),
537
- ),
538
- ),
528
+ const consequent = b.block(
529
+ transform_body(node.consequent.body, {
530
+ ...context,
531
+ state: { ...context.state, scope: context.state.scopes.get(node.consequent) },
532
+ }),
539
533
  );
534
+
535
+ let alternate = null;
536
+ if (node.alternate) {
537
+ const alternate_scope = context.state.scopes.get(node.alternate) || context.state.scope;
538
+ const alternate_body_nodes =
539
+ node.alternate.type === 'IfStatement' ? [node.alternate] : node.alternate.body;
540
+
541
+ alternate = b.block(
542
+ transform_body(alternate_body_nodes, {
543
+ ...context,
544
+ state: { ...context.state, scope: alternate_scope },
545
+ }),
546
+ );
547
+ }
548
+
549
+ context.state.init.push(b.if(context.visit(node.test), consequent, alternate));
540
550
  },
541
551
 
542
552
  Identifier(node, context) {
@@ -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
  */