ripple 0.2.139 → 0.2.141

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.139",
6
+ "version": "0.2.141",
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.139"
84
+ "ripple": "0.2.141"
85
85
  }
86
86
  }
@@ -169,14 +169,18 @@ function RipplePlugin(config) {
169
169
  }
170
170
 
171
171
  if (inComponent) {
172
- // Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
173
- // At component top-level (scopeStack.length <= 4), apply JSX detection logic
172
+ // Check if we're inside a nested function (arrow function, function expression, etc.)
173
+ // We need to distinguish between being inside a function vs just being in nested scopes
174
+ // (like for loops, if blocks, JSX elements, etc.)
175
+ const nestedFunctionContext = this.context.some((ctx) => ctx.token === 'function');
176
+
177
+ // Inside nested functions, treat < as relational/generic operator
174
178
  // BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
175
179
  const nextChar =
176
180
  this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
177
181
  const isClosingTag = nextChar === 47; // '/'
178
182
 
179
- if (this.scopeStack.length >= 5 && !isClosingTag) {
183
+ if (nestedFunctionContext && !isClosingTag) {
180
184
  // Inside function - treat as TypeScript generic, not JSX
181
185
  ++this.pos;
182
186
  return this.finishToken(tt.relational, '<');
@@ -987,6 +991,7 @@ function RipplePlugin(config) {
987
991
  this.finishNode(id, 'Identifier');
988
992
  node.name = id;
989
993
  node.value = id;
994
+ node.shorthand = true; // Mark as shorthand since name and value are the same
990
995
  this.next();
991
996
  this.expect(tt.braceR);
992
997
  return this.finishNode(node, 'Attribute');
@@ -109,7 +109,7 @@ const visitors = {
109
109
  const parent = context.path.at(-1);
110
110
 
111
111
  if (
112
- is_reference(node, /** @type {Node} */(parent)) &&
112
+ is_reference(node, /** @type {Node} */ (parent)) &&
113
113
  binding &&
114
114
  context.state.inside_server_block &&
115
115
  context.state.scope.server_block
@@ -144,7 +144,7 @@ const visitors = {
144
144
  }
145
145
 
146
146
  if (
147
- is_reference(node, /** @type {Node} */(parent)) &&
147
+ is_reference(node, /** @type {Node} */ (parent)) &&
148
148
  node.tracked &&
149
149
  binding?.node !== node
150
150
  ) {
@@ -155,7 +155,7 @@ const visitors = {
155
155
  }
156
156
 
157
157
  if (
158
- is_reference(node, /** @type {Node} */(parent)) &&
158
+ is_reference(node, /** @type {Node} */ (parent)) &&
159
159
  node.tracked &&
160
160
  binding?.node !== node
161
161
  ) {
@@ -191,7 +191,7 @@ const visitors = {
191
191
  error(
192
192
  `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`get(${node.object.name})\` or \`@${node.object.name}\` instead.`,
193
193
  context.state.analysis.module.filename,
194
- node.property
194
+ node.property,
195
195
  );
196
196
  }
197
197
  }
@@ -205,7 +205,7 @@ const visitors = {
205
205
  `Accessing a tracked object directly is not allowed, use the \`@\` prefix to read the value inside a tracked object - for example \`@${node.object.name}${node.property.type === 'Identifier' ? `.${node.property.name}` : ''}\``,
206
206
  context.state.analysis.module.filename,
207
207
  node,
208
- )
208
+ );
209
209
  }
210
210
  }
211
211
 
@@ -259,8 +259,11 @@ const visitors = {
259
259
  const callee = declarator.init.callee;
260
260
  // Check if it's a call to `track` or `tracked`
261
261
  if (
262
- (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'tracked')) ||
263
- (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && (callee.property.name === 'track' || callee.property.name === 'tracked'))
262
+ (callee.type === 'Identifier' &&
263
+ (callee.name === 'track' || callee.name === 'tracked')) ||
264
+ (callee.type === 'MemberExpression' &&
265
+ callee.property.type === 'Identifier' &&
266
+ (callee.property.name === 'track' || callee.property.name === 'tracked'))
264
267
  ) {
265
268
  binding.metadata = { ...binding.metadata, is_tracked_object: true };
266
269
  }
@@ -623,7 +626,6 @@ const visitors = {
623
626
  context.state.analysis.module.filename,
624
627
  node,
625
628
  );
626
-
627
629
  },
628
630
 
629
631
  Element(node, context) {
@@ -829,7 +831,7 @@ const visitors = {
829
831
  error(
830
832
  '`await` is not allowed in client-side control-flow statements',
831
833
  context.state.analysis.module.filename,
832
- node
834
+ node,
833
835
  );
834
836
  }
835
837
  }
@@ -190,9 +190,17 @@ const visitors = {
190
190
  const binding = context.state.scope.get(node.name);
191
191
  if (binding?.metadata?.is_dynamic_component) {
192
192
  // Capitalize the identifier for TypeScript
193
- const capitalizedName = node.name.charAt(0).toUpperCase() + node.name.slice(1);
194
- const capitalizedNode = { ...node, name: capitalizedName };
195
- return b.member(capitalizedNode, b.literal('#v'), true);
193
+ const capitalized_name = node.name.charAt(0).toUpperCase() + node.name.slice(1);
194
+ const capitalized_node = {
195
+ ...node,
196
+ name: capitalized_name,
197
+ metadata: {
198
+ ...node.metadata,
199
+ original_name: node.name,
200
+ is_capitalized: true,
201
+ },
202
+ };
203
+ return b.member(capitalized_node, b.literal('#v'), true);
196
204
  }
197
205
  return b.member(node, b.literal('#v'), true);
198
206
  }
@@ -525,13 +533,74 @@ const visitors = {
525
533
 
526
534
  VariableDeclarator(node, context) {
527
535
  // In TypeScript mode, capitalize identifiers that are used as dynamic components
528
- if (context.state.to_ts && node.id.type === 'Identifier') {
529
- const binding = context.state.scope.get(node.id.name);
530
- if (binding?.metadata?.is_dynamic_component) {
531
- const capitalizedName = node.id.name.charAt(0).toUpperCase() + node.id.name.slice(1);
536
+ if (context.state.to_ts) {
537
+ /**
538
+ * Recursively capitalize identifiers in patterns (ArrayPattern, ObjectPattern)
539
+ * @param {any} pattern - The pattern node to process
540
+ * @returns {any} The transformed pattern
541
+ */
542
+ const capitalize_pattern = (pattern) => {
543
+ if (pattern.type === 'Identifier') {
544
+ const binding = context.state.scope.get(pattern.name);
545
+ if (binding?.metadata?.is_dynamic_component) {
546
+ const capitalized_name = pattern.name.charAt(0).toUpperCase() + pattern.name.slice(1);
547
+ // Add metadata to track the original name for Volar mappings
548
+ return {
549
+ ...pattern,
550
+ name: capitalized_name,
551
+ metadata: {
552
+ ...pattern.metadata,
553
+ original_name: pattern.name,
554
+ is_capitalized: true,
555
+ },
556
+ };
557
+ }
558
+ return pattern;
559
+ } else if (pattern.type === 'ArrayPattern') {
560
+ return {
561
+ ...pattern,
562
+ elements: pattern.elements.map((element) =>
563
+ element ? capitalize_pattern(element) : element,
564
+ ),
565
+ };
566
+ } else if (pattern.type === 'ObjectPattern') {
567
+ return {
568
+ ...pattern,
569
+ properties: pattern.properties.map((prop) => {
570
+ if (prop.type === 'Property') {
571
+ return {
572
+ ...prop,
573
+ value: capitalize_pattern(prop.value),
574
+ };
575
+ } else if (prop.type === 'RestElement') {
576
+ return {
577
+ ...prop,
578
+ argument: capitalize_pattern(prop.argument),
579
+ };
580
+ }
581
+ return prop;
582
+ }),
583
+ };
584
+ } else if (pattern.type === 'RestElement') {
585
+ return {
586
+ ...pattern,
587
+ argument: capitalize_pattern(pattern.argument),
588
+ };
589
+ } else if (pattern.type === 'AssignmentPattern') {
590
+ return {
591
+ ...pattern,
592
+ left: capitalize_pattern(pattern.left),
593
+ right: context.visit(pattern.right),
594
+ };
595
+ }
596
+ return pattern;
597
+ };
598
+
599
+ const transformed_id = capitalize_pattern(node.id);
600
+ if (transformed_id !== node.id) {
532
601
  return {
533
602
  ...node,
534
- id: { ...node.id, name: capitalizedName },
603
+ id: transformed_id,
535
604
  init: node.init ? context.visit(node.init) : null,
536
605
  };
537
606
  }
@@ -938,7 +1007,7 @@ const visitors = {
938
1007
  let expression = visit(class_attribute.value, { ...state, metadata });
939
1008
 
940
1009
  const hash_arg = scoping_hash ? b.literal(scoping_hash) : undefined;
941
- const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg';
1010
+ const is_html = context.state.namespace === 'html' && node.id.name !== 'svg';
942
1011
 
943
1012
  if (metadata.tracking) {
944
1013
  local_updates.push(
@@ -1244,7 +1313,6 @@ const visitors = {
1244
1313
  /** @type {Expression} */ (context.visit(left)),
1245
1314
  /** @type {Expression} */ (context.visit(right)),
1246
1315
  ),
1247
- b.id('__block'),
1248
1316
  );
1249
1317
  }
1250
1318
 
@@ -1265,7 +1333,6 @@ const visitors = {
1265
1333
  ),
1266
1334
  /** @type {Expression} */ (context.visit(right)),
1267
1335
  ),
1268
- b.id('__block'),
1269
1336
  );
1270
1337
  }
1271
1338
 
@@ -1291,7 +1358,6 @@ const visitors = {
1291
1358
  node.prefix ? '_$_.update_pre_property' : '_$_.update_property',
1292
1359
  context.visit(argument.object, { ...context.state, metadata: { tracking: false } }),
1293
1360
  argument.computed ? context.visit(argument.property) : b.literal(argument.property.name),
1294
- b.id('__block'),
1295
1361
  node.operator === '--' ? b.literal(-1) : undefined,
1296
1362
  );
1297
1363
  }
@@ -1300,7 +1366,6 @@ const visitors = {
1300
1366
  return b.call(
1301
1367
  node.prefix ? '_$_.update_pre' : '_$_.update',
1302
1368
  context.visit(argument, { ...context.state, metadata: { tracking: null } }),
1303
- b.id('__block'),
1304
1369
  node.operator === '--' ? b.literal(-1) : undefined,
1305
1370
  );
1306
1371
  }
@@ -1309,7 +1374,6 @@ const visitors = {
1309
1374
  return b.call(
1310
1375
  node.prefix ? '_$_.update_pre' : '_$_.update',
1311
1376
  context.visit(argument.argument, { ...context.state, metadata: { tracking: null } }),
1312
- b.id('__block'),
1313
1377
  node.operator === '--' ? b.literal(-1) : undefined,
1314
1378
  );
1315
1379
  }
@@ -1740,7 +1804,10 @@ function transform_ts_child(node, context) {
1740
1804
  }
1741
1805
  jsx_name.loc = attr.name.loc || name.loc;
1742
1806
 
1743
- return b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
1807
+ const jsx_attr = b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
1808
+ // Preserve shorthand flag from parser (set for {identifier} syntax)
1809
+ jsx_attr.shorthand = attr.shorthand ?? false;
1810
+ return jsx_attr;
1744
1811
  } else if (attr.type === 'SpreadAttribute') {
1745
1812
  const metadata = { await: false };
1746
1813
  const argument = visit(attr.argument, { ...state, metadata });
@@ -1783,31 +1850,78 @@ function transform_ts_child(node, context) {
1783
1850
  closing_type = node.selfClosing ? undefined : type_expression;
1784
1851
  } else {
1785
1852
  opening_type = b.jsx_id(type_expression);
1786
- // Use node.id.loc if available, otherwise create a loc based on the element's position
1787
- opening_type.loc = node.id.loc || {
1788
- start: {
1789
- line: node.loc.start.line,
1790
- column: node.loc.start.column + 2, // After "<@"
1791
- },
1792
- end: {
1793
- line: node.loc.start.line,
1794
- column: node.loc.start.column + 2 + type_expression.length,
1795
- },
1796
- };
1797
-
1798
- if (!node.selfClosing) {
1799
- closing_type = b.jsx_id(type_expression);
1800
- closing_type.loc = {
1853
+ // For tracked identifiers (dynamic components), adjust the loc to skip the '@' prefix
1854
+ // and add metadata for mapping
1855
+ if (node.id.tracked && node.id.loc) {
1856
+ // The original identifier loc includes the '@', so we need to skip it
1857
+ opening_type.loc = {
1801
1858
  start: {
1802
- line: node.loc.end.line,
1803
- column: node.loc.end.column - type_expression.length - 1,
1859
+ line: node.id.loc.start.line,
1860
+ column: node.id.loc.start.column + 1, // Skip '@'
1861
+ },
1862
+ end: node.id.loc.end,
1863
+ };
1864
+ // Add metadata if this was capitalized
1865
+ if (node.metadata?.ts_name && node.metadata?.original_name) {
1866
+ opening_type.metadata = {
1867
+ original_name: node.metadata.original_name,
1868
+ is_capitalized: true,
1869
+ };
1870
+ }
1871
+ } else {
1872
+ // Use node.id.loc if available, otherwise create a loc based on the element's position
1873
+ opening_type.loc = node.id.loc || {
1874
+ start: {
1875
+ line: node.loc.start.line,
1876
+ column: node.loc.start.column + 2, // After "<@"
1804
1877
  },
1805
1878
  end: {
1806
- line: node.loc.end.line,
1807
- column: node.loc.end.column - 1,
1879
+ line: node.loc.start.line,
1880
+ column: node.loc.start.column + 2 + type_expression.length,
1808
1881
  },
1809
1882
  };
1810
1883
  }
1884
+
1885
+ if (!node.selfClosing) {
1886
+ closing_type = b.jsx_id(type_expression);
1887
+ // For tracked identifiers, also adjust closing tag location
1888
+ if (node.id.tracked && node.id.loc) {
1889
+ // Calculate position relative to closing tag
1890
+ // Format: </@identifier>
1891
+ const closing_tag_start = node.loc.end.column - type_expression.length - 3; // </@
1892
+ closing_type.loc = {
1893
+ start: {
1894
+ line: node.loc.end.line,
1895
+ column: closing_tag_start + 3, // Skip '</@'
1896
+ },
1897
+ end: {
1898
+ line: node.loc.end.line,
1899
+ column:
1900
+ closing_tag_start +
1901
+ 3 +
1902
+ (node.metadata?.original_name?.length || type_expression.length),
1903
+ },
1904
+ };
1905
+ // Add metadata if this was capitalized
1906
+ if (node.metadata?.ts_name && node.metadata?.original_name) {
1907
+ closing_type.metadata = {
1908
+ original_name: node.metadata.original_name,
1909
+ is_capitalized: true,
1910
+ };
1911
+ }
1912
+ } else {
1913
+ closing_type.loc = {
1914
+ start: {
1915
+ line: node.loc.end.line,
1916
+ column: node.loc.end.column - type_expression.length - 1,
1917
+ },
1918
+ end: {
1919
+ line: node.loc.end.line,
1920
+ column: node.loc.end.column - 1,
1921
+ },
1922
+ };
1923
+ }
1924
+ }
1811
1925
  }
1812
1926
 
1813
1927
  const jsxElement = b.jsx_element(
@@ -2219,6 +2333,22 @@ function create_tsx_with_typescript_support() {
2219
2333
 
2220
2334
  return {
2221
2335
  ...base_tsx,
2336
+ // Custom handler for ArrayPattern to ensure typeAnnotation is visited
2337
+ // esrap's TypeScript handler doesn't visit typeAnnotation for ArrayPattern (only for ObjectPattern)
2338
+ ArrayPattern(node, context) {
2339
+ context.write('[');
2340
+ for (let i = 0; i < node.elements.length; i++) {
2341
+ if (i > 0) context.write(', ');
2342
+ if (node.elements[i]) {
2343
+ context.visit(node.elements[i]);
2344
+ }
2345
+ }
2346
+ context.write(']');
2347
+ // Visit type annotation if present
2348
+ if (node.typeAnnotation) {
2349
+ context.visit(node.typeAnnotation);
2350
+ }
2351
+ },
2222
2352
  // Custom handler for FunctionDeclaration to support component->function mapping
2223
2353
  // Needed for volar mappings and intellisense on function or component keyword
2224
2354
  FunctionDeclaration(node, context) {
@@ -2396,12 +2526,13 @@ export function transform_client(filename, source, analysis, to_ts) {
2396
2526
 
2397
2527
  const language_handler = to_ts ? create_tsx_with_typescript_support() : tsx();
2398
2528
 
2399
- const js = /** @type {ReturnType<typeof print> & { post_processing_changes?: PostProcessingChanges, line_offsets?: number[] }} */ (
2400
- print(program, language_handler, {
2401
- sourceMapContent: source,
2402
- sourceMapSource: path.basename(filename),
2403
- })
2404
- );
2529
+ const js =
2530
+ /** @type {ReturnType<typeof print> & { post_processing_changes?: PostProcessingChanges, line_offsets?: number[] }} */ (
2531
+ print(program, language_handler, {
2532
+ sourceMapContent: source,
2533
+ sourceMapSource: path.basename(filename),
2534
+ })
2535
+ );
2405
2536
 
2406
2537
  // Post-process TypeScript output to remove 'declare' from function overload signatures
2407
2538
  // Function overload signatures in regular .ts files should not have 'declare' keyword
@@ -2427,7 +2558,10 @@ export function transform_client(filename, source, analysis, to_ts) {
2427
2558
  */
2428
2559
  const offset_to_line = (offset) => {
2429
2560
  for (let i = 0; i < line_offsets.length; i++) {
2430
- if (offset >= line_offsets[i] && (i === line_offsets.length - 1 || offset < line_offsets[i + 1])) {
2561
+ if (
2562
+ offset >= line_offsets[i] &&
2563
+ (i === line_offsets.length - 1 || offset < line_offsets[i + 1])
2564
+ ) {
2431
2565
  return i + 1;
2432
2566
  }
2433
2567
  }
@@ -2440,22 +2574,25 @@ export function transform_client(filename, source, analysis, to_ts) {
2440
2574
  // Remove 'export declare function' -> 'export function' (for overloads only, not implementations)
2441
2575
  // Match: export declare function name(...): type;
2442
2576
  // Don't match: export declare function name(...): type { (has body)
2443
- js.code = js.code.replace(/^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm, (match, p1, p2, offset) => {
2444
- const replacement = p1 + p2;
2445
- const line = offset_to_line(offset);
2446
- const delta = replacement.length - match.length; // negative (removing 'declare ')
2447
-
2448
- // Track first change offset and total delta per line
2449
- if (!line_deltas.has(line)) {
2450
- line_deltas.set(line, { offset, delta });
2451
- } else {
2452
- // Additional change on same line - accumulate delta
2453
- // @ts-ignore
2454
- line_deltas.get(line).delta += delta;
2455
- }
2577
+ js.code = js.code.replace(
2578
+ /^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm,
2579
+ (match, p1, p2, offset) => {
2580
+ const replacement = p1 + p2;
2581
+ const line = offset_to_line(offset);
2582
+ const delta = replacement.length - match.length; // negative (removing 'declare ')
2583
+
2584
+ // Track first change offset and total delta per line
2585
+ if (!line_deltas.has(line)) {
2586
+ line_deltas.set(line, { offset, delta });
2587
+ } else {
2588
+ // Additional change on same line - accumulate delta
2589
+ // @ts-ignore
2590
+ line_deltas.get(line).delta += delta;
2591
+ }
2456
2592
 
2457
- return replacement;
2458
- });
2593
+ return replacement;
2594
+ },
2595
+ );
2459
2596
 
2460
2597
  post_processing_changes = line_deltas;
2461
2598
  }
@@ -70,26 +70,6 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
70
70
  return line_offsets[line - 1] + column;
71
71
  };
72
72
 
73
- // Map to track capitalized names: original name -> capitalized name
74
- /** @type {Map<string, string>} */
75
- const capitalized_names = new Map();
76
- // Reverse map: capitalized name -> original name
77
- /** @type {Map<string, string>} */
78
- const reverse_capitalized_names = new Map();
79
-
80
- // Pre-walk to collect capitalized names from JSXElement nodes (transformed AST)
81
- // These are identifiers that are used as dynamic components/elements
82
- walk(ast, null, {
83
- _(node, { next }) {
84
- // Check JSXElement nodes with metadata (preserved from Element nodes)
85
- if (node.type === 'JSXElement' && node.metadata?.ts_name && node.metadata?.original_name) {
86
- capitalized_names.set(node.metadata.original_name, node.metadata.ts_name);
87
- reverse_capitalized_names.set(node.metadata.ts_name, node.metadata.original_name);
88
- }
89
- next();
90
- }
91
- });
92
-
93
73
  const adjusted_source_map = build_source_to_generated_map(
94
74
  esrap_source_map,
95
75
  post_processing_changes,
@@ -110,51 +90,30 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
110
90
  walk(ast, null, {
111
91
  _(node, { visit }) {
112
92
  // Collect key node types: Identifiers, Literals, and JSX Elements
113
- // Skip nodes without .loc (synthesized during transformation, not in original source)
114
- if (node.type === 'Identifier' && node.name) {
115
- if (node.loc) {
93
+ if (node.type === 'Identifier') {
94
+ // Only create mappings for identifiers with location info (from source)
95
+ // Synthesized identifiers (created by builders) don't have .loc and are skipped
96
+ if (node.name && node.loc) {
116
97
  // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
117
98
  if (node.metadata?.tracked_shorthand) {
118
99
  tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name, loc: node.loc });
100
+ } else if (node.metadata?.is_capitalized) {
101
+ // This identifier was capitalized during transformation
102
+ // Map the original lowercase name to the capitalized generated name
103
+ tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
119
104
  } else {
120
- // Check if this identifier was capitalized (reverse lookup)
121
- const original_name = reverse_capitalized_names.get(node.name);
122
- if (original_name) {
123
- // This is a capitalized name in generated code, map to lowercase in source
124
- tokens.push({ source: original_name, generated: node.name, loc: node.loc });
125
- } else {
126
- // Check if this identifier should be capitalized (forward lookup)
127
- const cap_name = capitalized_names.get(node.name);
128
- if (cap_name) {
129
- tokens.push({ source: node.name, generated: cap_name, loc: node.loc });
130
- } else {
131
- // Check if this identifier should be capitalized (forward lookup)
132
- const cap_name = capitalized_names.get(node.name);
133
- if (cap_name) {
134
- tokens.push({ source: node.name, generated: cap_name, loc: node.loc });
135
- } else {
136
- // Store token with .loc for accurate positioning
137
- tokens.push({ source: node.name, generated: node.name, loc: node.loc });
138
- }
139
- }
140
- }
105
+ // No transformation - source and generated names are the same
106
+ tokens.push({ source: node.name, generated: node.name, loc: node.loc });
141
107
  }
142
108
  }
143
109
  return; // Leaf node, don't traverse further
144
- } else if (node.type === 'JSXIdentifier' && node.name) {
145
- if (node.loc) {
146
- // Check if this was capitalized (reverse lookup)
147
- const originalName = reverse_capitalized_names.get(node.name);
148
- if (originalName) {
149
- tokens.push({ source: originalName, generated: node.name, loc: node.loc });
110
+ } else if (node.type === 'JSXIdentifier') {
111
+ // JSXIdentifiers can also be capitalized (for dynamic components)
112
+ if (node.loc && node.name) {
113
+ if (node.metadata?.is_capitalized) {
114
+ tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
150
115
  } else {
151
- // Check if this should be capitalized (forward lookup)
152
- const capitalizedName = capitalized_names.get(node.name);
153
- if (capitalizedName) {
154
- tokens.push({ source: node.name, generated: capitalizedName, loc: node.loc });
155
- } else {
156
- tokens.push({ source: node.name, generated: node.name, loc: node.loc });
157
- }
116
+ tokens.push({ source: node.name, generated: node.name, loc: node.loc });
158
117
  }
159
118
  }
160
119
  return; // Leaf node, don't traverse further
@@ -242,11 +201,18 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
242
201
  return;
243
202
  } else if (node.type === 'JSXAttribute') {
244
203
  // Visit name and value in source order
245
- if (node.name) {
246
- visit(node.name);
247
- }
248
- if (node.value) {
249
- visit(node.value);
204
+ // For shorthand attributes ({ count }), key and value are the same node, only visit once
205
+ if (node.shorthand) {
206
+ if (node.value) {
207
+ visit(node.value);
208
+ }
209
+ } else {
210
+ if (node.name) {
211
+ visit(node.name);
212
+ }
213
+ if (node.value) {
214
+ visit(node.value);
215
+ }
250
216
  }
251
217
  return;
252
218
  } else if (node.type === 'JSXSpreadAttribute') {
@@ -282,19 +248,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
282
248
  // 3. Push closing tag name (not visited by AST walker)
283
249
  if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
284
250
  const closingNameNode = node.closingElement.name;
285
- const closingName = closingNameNode.name;
286
- // Check if this was capitalized (reverse lookup)
287
- const originalName = reverse_capitalized_names.get(closingName);
288
- if (originalName) {
289
- tokens.push({ source: originalName, generated: closingName, loc: closingNameNode.loc });
251
+ if (closingNameNode.metadata?.is_capitalized) {
252
+ tokens.push({ source: closingNameNode.metadata.original_name, generated: closingNameNode.name, loc: closingNameNode.loc });
290
253
  } else {
291
- // Check if this should be capitalized (forward lookup)
292
- const capitalizedName = capitalized_names.get(closingName);
293
- if (capitalizedName) {
294
- tokens.push({ source: closingName, generated: capitalizedName, loc: closingNameNode.loc });
295
- } else {
296
- tokens.push({ source: closingName, generated: closingName, loc: closingNameNode.loc });
297
- }
254
+ tokens.push({ source: closingNameNode.name, generated: closingNameNode.name, loc: closingNameNode.loc });
298
255
  }
299
256
  }
300
257