ripple 0.2.114 → 0.2.115

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.115",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -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 {
@@ -441,6 +450,22 @@ const visitors = {
441
450
  return context.next();
442
451
  },
443
452
 
453
+ VariableDeclarator(node, context) {
454
+ // In TypeScript mode, capitalize identifiers that are used as dynamic components
455
+ if (context.state.to_ts && node.id.type === 'Identifier') {
456
+ const binding = context.state.scope.get(node.id.name);
457
+ if (binding?.metadata?.is_dynamic_component) {
458
+ const capitalizedName = node.id.name.charAt(0).toUpperCase() + node.id.name.slice(1);
459
+ return {
460
+ ...node,
461
+ id: { ...node.id, name: capitalizedName },
462
+ init: node.init ? context.visit(node.init) : null
463
+ };
464
+ }
465
+ }
466
+ return context.next();
467
+ },
468
+
444
469
  FunctionDeclaration(node, context) {
445
470
  return visit_function(node, context);
446
471
  },
@@ -1397,7 +1422,8 @@ function transform_ts_child(node, context) {
1397
1422
  // Do we need to do something special here?
1398
1423
  state.init.push(b.stmt(visit(node.expression, { ...state })));
1399
1424
  } else if (node.type === 'Element') {
1400
- const type = node.id.name;
1425
+ // Use capitalized name for dynamic components/elements in TypeScript output
1426
+ const type = node.metadata?.ts_name || node.id.name;
1401
1427
  const children = [];
1402
1428
  let has_children_props = false;
1403
1429
 
@@ -1462,33 +1488,44 @@ function transform_ts_child(node, context) {
1462
1488
  }
1463
1489
  }
1464
1490
 
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;
1491
+ const opening_type = b.jsx_id(type);
1492
+ // Use node.id.loc if available, otherwise create a loc based on the element's position
1493
+ opening_type.loc = node.id.loc || {
1494
+ start: {
1495
+ line: node.loc.start.line,
1496
+ column: node.loc.start.column + 2, // After "<@"
1497
+ },
1498
+ end: {
1499
+ line: node.loc.start.line,
1500
+ column: node.loc.start.column + 2 + type.length,
1501
+ },
1502
+ };
1480
1503
 
1481
1504
  let closing_type = undefined;
1482
1505
 
1483
1506
  if (!node.selfClosing) {
1484
- closing_type = tracked_type
1485
- ? b.jsx_id(tracked_type)
1486
- : b.jsx_id(type);
1507
+ closing_type = b.jsx_id(type);
1508
+ closing_type.loc = {
1509
+ start: {
1510
+ line: node.loc.end.line,
1511
+ column: node.loc.end.column - type.length - 1,
1512
+ },
1513
+ end: {
1514
+ line: node.loc.end.line,
1515
+ column: node.loc.end.column - 1,
1516
+ },
1517
+ };
1487
1518
  }
1488
1519
 
1489
- state.init.push(
1490
- b.stmt(b.jsx_element(opening_type, attributes, children, node.selfClosing, closing_type)),
1491
- );
1520
+ const jsxElement = b.jsx_element(opening_type, attributes, children, node.selfClosing, closing_type);
1521
+ // Preserve metadata from Element node for mapping purposes
1522
+ if (node.metadata && (node.metadata.ts_name || node.metadata.original_name)) {
1523
+ jsxElement.metadata = {
1524
+ ts_name: node.metadata.ts_name,
1525
+ original_name: node.metadata.original_name
1526
+ };
1527
+ }
1528
+ state.init.push(b.stmt(jsxElement));
1492
1529
  } else if (node.type === 'IfStatement') {
1493
1530
  const consequent_scope = context.state.scopes.get(node.consequent);
1494
1531
  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,66 @@ 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 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 });
175
+ } 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 });
180
+ } else {
181
+ tokens.push(node.name);
182
+ }
183
+ }
184
+ }
108
185
  return; // Leaf node, don't traverse further
109
186
  } else if (node.type === 'JSXIdentifier' && node.name) {
110
- tokens.push(node.name);
187
+ if (node.loc) {
188
+ // Check if this was capitalized (reverse lookup)
189
+ const originalName = reverseCapitalizedNames.get(node.name);
190
+ if (originalName) {
191
+ tokens.push({ source: originalName, generated: node.name });
192
+ } else {
193
+ // Check if this should be capitalized (forward lookup)
194
+ const capitalizedName = capitalizedNames.get(node.name);
195
+ if (capitalizedName) {
196
+ tokens.push({ source: node.name, generated: capitalizedName });
197
+ } else {
198
+ tokens.push(node.name);
199
+ }
200
+ }
201
+ }
111
202
  return; // Leaf node, don't traverse further
112
203
  } else if (node.type === 'Literal' && node.raw) {
113
- tokens.push(node.raw);
204
+ if (node.loc) {
205
+ tokens.push(node.raw);
206
+ }
114
207
  return; // Leaf node, don't traverse further
115
208
  } else if (node.type === 'ImportDeclaration') {
209
+ // Collect import declaration range for full-statement mapping
210
+ // 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 });
213
+ }
214
+
116
215
  // Visit specifiers in source order
117
216
  if (node.specifiers) {
118
217
  for (const specifier of node.specifiers) {
@@ -209,7 +308,20 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
209
308
 
210
309
  // 3. Push closing tag name (not visited by AST walker)
211
310
  if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
212
- tokens.push(node.closingElement.name.name);
311
+ const closingName = node.closingElement.name.name;
312
+ // Check if this was capitalized (reverse lookup)
313
+ const originalName = reverseCapitalizedNames.get(closingName);
314
+ if (originalName) {
315
+ tokens.push({ source: originalName, generated: closingName });
316
+ } else {
317
+ // Check if this should be capitalized (forward lookup)
318
+ const capitalizedName = capitalizedNames.get(closingName);
319
+ if (capitalizedName) {
320
+ tokens.push({ source: closingName, generated: capitalizedName });
321
+ } else {
322
+ tokens.push(closingName);
323
+ }
324
+ }
213
325
  }
214
326
 
215
327
  return;
@@ -988,23 +1100,65 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
988
1100
  });
989
1101
 
990
1102
  // Process each token in order
991
- for (const text of tokens) {
992
- const sourcePos = findInSource(text);
993
- const genPos = findInGenerated(text);
1103
+ for (const token of tokens) {
1104
+ let sourceText, generatedText;
1105
+
1106
+ if (typeof token === 'string') {
1107
+ sourceText = token;
1108
+ generatedText = token;
1109
+ } else {
1110
+ // Token with different source and generated names
1111
+ sourceText = token.source;
1112
+ generatedText = token.generated;
1113
+ }
1114
+
1115
+ const sourcePos = findInSource(sourceText);
1116
+ const genPos = findInGenerated(generatedText);
994
1117
 
995
1118
  if (sourcePos !== null && genPos !== null) {
996
1119
  mappings.push({
997
1120
  sourceOffsets: [sourcePos],
998
1121
  generatedOffsets: [genPos],
999
- lengths: [text.length],
1122
+ lengths: [sourceText.length],
1000
1123
  data: mapping_data,
1001
1124
  });
1002
1125
  }
1003
1126
  }
1004
1127
 
1128
+ // Add full-statement mappings for import declarations
1129
+ // TypeScript reports unused import diagnostics covering the entire import statement
1130
+ // 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
+ });
1142
+ }
1143
+
1005
1144
  // Sort mappings by source offset
1006
1145
  mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
1007
1146
 
1147
+ // Add a mapping for the very beginning of the file to handle import additions
1148
+ // This ensures that code actions adding imports at the top work correctly
1149
+ if (mappings.length > 0 && mappings[0].sourceOffsets[0] > 0) {
1150
+ mappings.unshift({
1151
+ sourceOffsets: [0],
1152
+ generatedOffsets: [0],
1153
+ lengths: [1],
1154
+ data: {
1155
+ ...mapping_data,
1156
+ codeActions: true, // auto-import
1157
+ rename: false, // avoid rename for a “dummy” mapping
1158
+ }
1159
+ });
1160
+ }
1161
+
1008
1162
  return {
1009
1163
  code: generated_code,
1010
1164
  mappings,
package/types/index.d.ts CHANGED
@@ -73,7 +73,23 @@ declare global {
73
73
 
74
74
  export declare function createRefKey(): symbol;
75
75
 
76
- export type Tracked<V> = { '#v': V };
76
+ // Base Tracked interface - all tracked values have a '#v' property containing the actual value
77
+ export interface Tracked<V> { '#v': V; }
78
+
79
+ // Augment Tracked to be callable when V is a Component
80
+ // This allows <@Something /> to work in JSX when Something is Tracked<Component>
81
+ export interface Tracked<V> {
82
+ (props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
83
+ }
84
+
85
+ // Helper type to infer component type from a function that returns a component
86
+ // If T is a function returning a Component, extract the Component type itself, not the return type (void)
87
+ export type InferComponent<T> =
88
+ T extends () => infer R
89
+ ? R extends Component<any>
90
+ ? R
91
+ : T
92
+ : T;
77
93
 
78
94
  export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
79
95
  export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
@@ -90,7 +106,10 @@ type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
90
106
  type SplitResult<T extends Props, K extends readonly (keyof T)[]> =
91
107
  [...PickKeys<T, K>, Tracked<RestKeys<T, K>>];
92
108
 
93
- export declare function track<V>(value?: V | (() => V), get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
109
+ // Overload for function values - infers the return type of the function
110
+ export declare function track<V>(value: () => V, get?: (v: InferComponent<V>) => InferComponent<V>, set?: (next: InferComponent<V>, prev: InferComponent<V>) => InferComponent<V>): Tracked<InferComponent<V>>;
111
+ // Overload for non-function values
112
+ export declare function track<V>(value?: V, get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
94
113
 
95
114
  export declare function trackSplit<V extends Props, const K extends readonly (keyof V)[]>(
96
115
  value: V,