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 +1 -1
- package/src/compiler/phases/1-parse/index.js +79 -0
- package/src/compiler/phases/2-analyze/index.js +17 -0
- package/src/compiler/phases/3-transform/client/index.js +109 -26
- package/src/compiler/phases/3-transform/segments.js +168 -9
- package/src/compiler/types/index.d.ts +16 -0
- package/src/runtime/index-client.js +18 -184
- package/src/runtime/internal/client/bindings.js +443 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/map.js +11 -1
- package/src/runtime/set.js +11 -1
- package/tests/client/map.test.ripple +81 -0
- package/tests/client/set.test.ripple +81 -0
- package/types/index.d.ts +100 -39
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.
|
|
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.
|
|
345
|
-
b.
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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 =
|
|
1485
|
-
|
|
1486
|
-
:
|
|
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
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
992
|
-
|
|
993
|
-
|
|
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: [
|
|
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
|
*/
|