ripple 0.2.131 → 0.2.133
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 +2 -2
- package/src/compiler/phases/1-parse/index.js +217 -28
- package/src/compiler/phases/2-analyze/index.js +10 -6
- package/src/compiler/phases/3-transform/client/index.js +203 -46
- package/src/compiler/phases/3-transform/segments.js +10 -0
- package/src/compiler/types/index.d.ts +12 -0
- package/src/runtime/internal/client/compat.js +8 -0
- package/src/runtime/internal/client/index.js +2 -0
- package/tests/client/compiler/compiler.basic.test.ripple +117 -1
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.133",
|
|
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.
|
|
84
|
+
"ripple": "0.2.133"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
/** @import { Program } from 'estree' */
|
|
2
3
|
/** @import {
|
|
3
4
|
* CommentWithLocation,
|
|
@@ -28,6 +29,14 @@ function convert_from_jsx(node) {
|
|
|
28
29
|
return node;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function isWhitespaceTextNode(node) {
|
|
33
|
+
if (!node || node.type !== 'Text') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const value = typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
|
|
37
|
+
return /^\s*$/.test(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/**
|
|
32
41
|
* Acorn parser plugin for Ripple syntax extensions
|
|
33
42
|
* @param {RipplePluginConfig} [config] - Plugin configuration
|
|
@@ -42,6 +51,37 @@ function RipplePlugin(config) {
|
|
|
42
51
|
class RippleParser extends Parser {
|
|
43
52
|
/** @type {any[]} */
|
|
44
53
|
#path = [];
|
|
54
|
+
#commentContextId = 0;
|
|
55
|
+
|
|
56
|
+
#createCommentMetadata() {
|
|
57
|
+
if (this.#path.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const container = this.#path[this.#path.length - 1];
|
|
62
|
+
if (!container || container.type !== 'Element') {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const children = Array.isArray(container.children) ? container.children : [];
|
|
67
|
+
const hasMeaningfulChildren = children.some((child) => child && !isWhitespaceTextNode(child));
|
|
68
|
+
|
|
69
|
+
if (hasMeaningfulChildren) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
container.metadata ??= {};
|
|
74
|
+
if (container.metadata.commentContainerId === undefined) {
|
|
75
|
+
container.metadata.commentContainerId = ++this.#commentContextId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
containerId: container.metadata.commentContainerId,
|
|
80
|
+
containerType: container.type,
|
|
81
|
+
childIndex: children.length,
|
|
82
|
+
beforeMeaningfulChild: !hasMeaningfulChildren,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
45
85
|
|
|
46
86
|
/**
|
|
47
87
|
* Helper method to get the element name from a JSX identifier or member expression
|
|
@@ -107,7 +147,8 @@ function RipplePlugin(config) {
|
|
|
107
147
|
// Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
|
|
108
148
|
// At component top-level (scopeStack.length <= 4), apply JSX detection logic
|
|
109
149
|
// BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
|
|
110
|
-
const nextChar =
|
|
150
|
+
const nextChar =
|
|
151
|
+
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
111
152
|
const isClosingTag = nextChar === 47; // '/'
|
|
112
153
|
|
|
113
154
|
if (this.scopeStack.length >= 5 && !isClosingTag) {
|
|
@@ -169,15 +210,19 @@ function RipplePlugin(config) {
|
|
|
169
210
|
|
|
170
211
|
// Check if this is #Map or #Set
|
|
171
212
|
if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
|
|
172
|
-
const charAfter =
|
|
173
|
-
|
|
213
|
+
const charAfter =
|
|
214
|
+
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
215
|
+
if (charAfter === 40) {
|
|
216
|
+
// ( character
|
|
174
217
|
this.pos += 4; // consume '#Map'
|
|
175
218
|
return this.finishToken(tt.name, '#Map');
|
|
176
219
|
}
|
|
177
220
|
}
|
|
178
221
|
if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
|
|
179
|
-
const charAfter =
|
|
180
|
-
|
|
222
|
+
const charAfter =
|
|
223
|
+
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
224
|
+
if (charAfter === 40) {
|
|
225
|
+
// ( character
|
|
181
226
|
this.pos += 4; // consume '#Set'
|
|
182
227
|
return this.finishToken(tt.name, '#Set');
|
|
183
228
|
}
|
|
@@ -206,12 +251,18 @@ function RipplePlugin(config) {
|
|
|
206
251
|
// Check if this is an invalid #Identifier pattern
|
|
207
252
|
// Valid patterns: #[, #{, #Map(, #Set(, #server
|
|
208
253
|
// If we see # followed by an uppercase letter that isn't Map or Set, it's an error
|
|
209
|
-
if (nextChar >= 65 && nextChar <= 90) {
|
|
254
|
+
if (nextChar >= 65 && nextChar <= 90) {
|
|
255
|
+
// A-Z
|
|
210
256
|
// Extract the identifier name
|
|
211
257
|
let identEnd = this.pos + 1;
|
|
212
258
|
while (identEnd < this.input.length) {
|
|
213
259
|
const ch = this.input.charCodeAt(identEnd);
|
|
214
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
(ch >= 65 && ch <= 90) ||
|
|
262
|
+
(ch >= 97 && ch <= 122) ||
|
|
263
|
+
(ch >= 48 && ch <= 57) ||
|
|
264
|
+
ch === 95
|
|
265
|
+
) {
|
|
215
266
|
// A-Z, a-z, 0-9, _
|
|
216
267
|
identEnd++;
|
|
217
268
|
} else {
|
|
@@ -220,7 +271,10 @@ function RipplePlugin(config) {
|
|
|
220
271
|
}
|
|
221
272
|
const identName = this.input.slice(this.pos + 1, identEnd);
|
|
222
273
|
if (identName !== 'Map' && identName !== 'Set') {
|
|
223
|
-
this.raise(
|
|
274
|
+
this.raise(
|
|
275
|
+
this.pos,
|
|
276
|
+
`Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
|
|
277
|
+
);
|
|
224
278
|
}
|
|
225
279
|
}
|
|
226
280
|
}
|
|
@@ -940,9 +994,13 @@ function RipplePlugin(config) {
|
|
|
940
994
|
return this.finishNode(node, 'JSXIdentifier');
|
|
941
995
|
}
|
|
942
996
|
|
|
943
|
-
// Override jsx_parseElementName to support @ syntax in member expressions
|
|
944
997
|
jsx_parseElementName() {
|
|
945
|
-
let node = this.
|
|
998
|
+
let node = this.jsx_parseNamespacedName();
|
|
999
|
+
|
|
1000
|
+
if (node.type === 'JSXNamespacedName') {
|
|
1001
|
+
return node;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
946
1004
|
if (this.eat(tt.dot)) {
|
|
947
1005
|
let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
|
|
948
1006
|
memberExpr.object = node;
|
|
@@ -1019,11 +1077,15 @@ function RipplePlugin(config) {
|
|
|
1019
1077
|
}
|
|
1020
1078
|
|
|
1021
1079
|
jsx_readToken() {
|
|
1080
|
+
const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
|
|
1081
|
+
if (inside_tsx_compat) {
|
|
1082
|
+
return super.jsx_readToken();
|
|
1083
|
+
}
|
|
1022
1084
|
let out = '',
|
|
1023
1085
|
chunkStart = this.pos;
|
|
1024
1086
|
const tok = this.acornTypeScript.tokTypes;
|
|
1025
1087
|
|
|
1026
|
-
|
|
1088
|
+
while (true) {
|
|
1027
1089
|
if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
|
|
1028
1090
|
let ch = this.input.charCodeAt(this.pos);
|
|
1029
1091
|
|
|
@@ -1061,6 +1123,7 @@ function RipplePlugin(config) {
|
|
|
1061
1123
|
|
|
1062
1124
|
// Call onComment if it exists
|
|
1063
1125
|
if (this.options.onComment) {
|
|
1126
|
+
const metadata = this.#createCommentMetadata();
|
|
1064
1127
|
this.options.onComment(
|
|
1065
1128
|
false,
|
|
1066
1129
|
commentText,
|
|
@@ -1068,6 +1131,7 @@ function RipplePlugin(config) {
|
|
|
1068
1131
|
commentEnd,
|
|
1069
1132
|
startLoc,
|
|
1070
1133
|
endLoc,
|
|
1134
|
+
metadata,
|
|
1071
1135
|
);
|
|
1072
1136
|
}
|
|
1073
1137
|
|
|
@@ -1098,6 +1162,7 @@ function RipplePlugin(config) {
|
|
|
1098
1162
|
|
|
1099
1163
|
// Call onComment if it exists
|
|
1100
1164
|
if (this.options.onComment) {
|
|
1165
|
+
const metadata = this.#createCommentMetadata();
|
|
1101
1166
|
this.options.onComment(
|
|
1102
1167
|
true,
|
|
1103
1168
|
commentText,
|
|
@@ -1105,6 +1170,7 @@ function RipplePlugin(config) {
|
|
|
1105
1170
|
commentEnd,
|
|
1106
1171
|
startLoc,
|
|
1107
1172
|
endLoc,
|
|
1173
|
+
metadata,
|
|
1108
1174
|
);
|
|
1109
1175
|
}
|
|
1110
1176
|
|
|
@@ -1178,10 +1244,29 @@ function RipplePlugin(config) {
|
|
|
1178
1244
|
element.start = position.index;
|
|
1179
1245
|
element.loc.start = position;
|
|
1180
1246
|
element.metadata = {};
|
|
1181
|
-
element.type = 'Element';
|
|
1182
|
-
this.#path.push(element);
|
|
1183
1247
|
element.children = [];
|
|
1184
1248
|
const open = this.jsx_parseOpeningElementAt();
|
|
1249
|
+
|
|
1250
|
+
// Check if this is a namespaced element (tsx:react)
|
|
1251
|
+
const is_tsx_compat = open.name.type === 'JSXNamespacedName';
|
|
1252
|
+
|
|
1253
|
+
if (is_tsx_compat) {
|
|
1254
|
+
element.type = 'TsxCompat';
|
|
1255
|
+
element.kind = open.name.name.name; // e.g., "react" from "tsx:react"
|
|
1256
|
+
|
|
1257
|
+
if (open.selfClosing) {
|
|
1258
|
+
const tagName = open.name.namespace.name + ':' + open.name.name.name;
|
|
1259
|
+
this.raise(
|
|
1260
|
+
open.start,
|
|
1261
|
+
`TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
element.type = 'Element';
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
this.#path.push(element);
|
|
1269
|
+
|
|
1185
1270
|
for (const attr of open.attributes) {
|
|
1186
1271
|
if (attr.type === 'JSXAttribute') {
|
|
1187
1272
|
attr.type = 'Attribute';
|
|
@@ -1195,14 +1280,18 @@ function RipplePlugin(config) {
|
|
|
1195
1280
|
}
|
|
1196
1281
|
}
|
|
1197
1282
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1283
|
+
|
|
1284
|
+
if (!is_tsx_compat) {
|
|
1285
|
+
if (open.name.type === 'JSXIdentifier') {
|
|
1286
|
+
open.name.type = 'Identifier';
|
|
1287
|
+
}
|
|
1288
|
+
element.id = convert_from_jsx(open.name);
|
|
1289
|
+
element.selfClosing = open.selfClosing;
|
|
1200
1290
|
}
|
|
1201
1291
|
|
|
1202
|
-
element.id = convert_from_jsx(open.name);
|
|
1203
1292
|
element.attributes = open.attributes;
|
|
1204
|
-
element.
|
|
1205
|
-
element.metadata =
|
|
1293
|
+
element.metadata ??= {};
|
|
1294
|
+
element.metadata.commentContainerId = ++this.#commentContextId;
|
|
1206
1295
|
|
|
1207
1296
|
if (element.selfClosing) {
|
|
1208
1297
|
this.#path.pop();
|
|
@@ -1295,10 +1384,40 @@ function RipplePlugin(config) {
|
|
|
1295
1384
|
this.parseTemplateBody(element.children);
|
|
1296
1385
|
this.exitScope();
|
|
1297
1386
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1387
|
+
if (element.type === 'TsxCompat') {
|
|
1388
|
+
this.#path.pop();
|
|
1389
|
+
|
|
1390
|
+
const raise_error = () => {
|
|
1391
|
+
this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
this.next();
|
|
1395
|
+
// we should expect to see </tsx:kind>
|
|
1396
|
+
if (this.value !== '/') {
|
|
1397
|
+
raise_error();
|
|
1398
|
+
}
|
|
1399
|
+
this.next();
|
|
1400
|
+
if (this.value !== 'tsx') {
|
|
1401
|
+
raise_error();
|
|
1402
|
+
}
|
|
1403
|
+
this.next();
|
|
1404
|
+
if (this.type.label !== ':') {
|
|
1405
|
+
raise_error();
|
|
1406
|
+
}
|
|
1407
|
+
this.next();
|
|
1408
|
+
if (this.value !== element.kind) {
|
|
1409
|
+
raise_error();
|
|
1410
|
+
}
|
|
1411
|
+
this.next();
|
|
1412
|
+
if (this.type.label !== 'jsxTagEnd') {
|
|
1413
|
+
raise_error();
|
|
1414
|
+
}
|
|
1415
|
+
this.next();
|
|
1416
|
+
} else if (this.#path[this.#path.length - 1] === element) {
|
|
1417
|
+
// Check if this element was properly closed
|
|
1418
|
+
// If we reach here and this element is still in the path, it means it was never closed
|
|
1301
1419
|
const tagName = this.getElementName(element.id);
|
|
1420
|
+
|
|
1302
1421
|
this.raise(
|
|
1303
1422
|
this.start,
|
|
1304
1423
|
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
@@ -1314,13 +1433,14 @@ function RipplePlugin(config) {
|
|
|
1314
1433
|
}
|
|
1315
1434
|
}
|
|
1316
1435
|
|
|
1317
|
-
this.finishNode(element,
|
|
1436
|
+
this.finishNode(element, element.type);
|
|
1318
1437
|
return element;
|
|
1319
1438
|
}
|
|
1320
1439
|
|
|
1321
1440
|
parseTemplateBody(body) {
|
|
1322
|
-
|
|
1441
|
+
const inside_func =
|
|
1323
1442
|
this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
|
|
1443
|
+
const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
|
|
1324
1444
|
|
|
1325
1445
|
if (!inside_func) {
|
|
1326
1446
|
if (this.type.label === 'return') {
|
|
@@ -1334,6 +1454,19 @@ function RipplePlugin(config) {
|
|
|
1334
1454
|
}
|
|
1335
1455
|
}
|
|
1336
1456
|
|
|
1457
|
+
if (inside_tsx_compat) {
|
|
1458
|
+
this.exprAllowed = true;
|
|
1459
|
+
|
|
1460
|
+
while (true) {
|
|
1461
|
+
const node = super.parseExpression();
|
|
1462
|
+
body.push(node);
|
|
1463
|
+
|
|
1464
|
+
if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1337
1470
|
if (this.type.label === '{') {
|
|
1338
1471
|
const node = this.jsx_parseExpressionContainer();
|
|
1339
1472
|
node.type = node.html ? 'Html' : 'Text';
|
|
@@ -1350,12 +1483,32 @@ function RipplePlugin(config) {
|
|
|
1350
1483
|
|
|
1351
1484
|
// Validate that the closing tag matches the opening tag
|
|
1352
1485
|
const currentElement = this.#path[this.#path.length - 1];
|
|
1353
|
-
if (
|
|
1486
|
+
if (
|
|
1487
|
+
!currentElement ||
|
|
1488
|
+
(currentElement.type !== 'Element' && currentElement.type !== 'TsxCompat')
|
|
1489
|
+
) {
|
|
1354
1490
|
this.raise(this.start, 'Unexpected closing tag');
|
|
1355
1491
|
}
|
|
1356
1492
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1493
|
+
let openingTagName;
|
|
1494
|
+
let closingTagName;
|
|
1495
|
+
|
|
1496
|
+
if (currentElement.type === 'TsxCompat') {
|
|
1497
|
+
if (closingTag.type === 'JSXNamespacedName') {
|
|
1498
|
+
openingTagName = 'tsx:' + currentElement.kind;
|
|
1499
|
+
closingTagName = closingTag.namespace.name + ':' + closingTag.name.name;
|
|
1500
|
+
} else {
|
|
1501
|
+
openingTagName = 'tsx:' + currentElement.kind;
|
|
1502
|
+
closingTagName = this.getElementName(closingTag);
|
|
1503
|
+
}
|
|
1504
|
+
} else {
|
|
1505
|
+
// Regular Element node
|
|
1506
|
+
openingTagName = this.getElementName(currentElement.id);
|
|
1507
|
+
closingTagName =
|
|
1508
|
+
closingTag.type === 'JSXNamespacedName'
|
|
1509
|
+
? closingTag.namespace.name + ':' + closingTag.name.name
|
|
1510
|
+
: this.getElementName(closingTag);
|
|
1511
|
+
}
|
|
1359
1512
|
|
|
1360
1513
|
if (openingTagName !== closingTagName) {
|
|
1361
1514
|
this.raise(
|
|
@@ -1535,7 +1688,7 @@ function RipplePlugin(config) {
|
|
|
1535
1688
|
*/
|
|
1536
1689
|
function get_comment_handlers(source, comments, index = 0) {
|
|
1537
1690
|
return {
|
|
1538
|
-
onComment: (block, value, start, end, start_loc, end_loc) => {
|
|
1691
|
+
onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
|
|
1539
1692
|
if (block && /\n/.test(value)) {
|
|
1540
1693
|
let a = start;
|
|
1541
1694
|
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
|
@@ -1556,6 +1709,7 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1556
1709
|
start: /** @type {import('acorn').Position} */ (start_loc),
|
|
1557
1710
|
end: /** @type {import('acorn').Position} */ (end_loc),
|
|
1558
1711
|
},
|
|
1712
|
+
context: metadata ?? null,
|
|
1559
1713
|
});
|
|
1560
1714
|
},
|
|
1561
1715
|
add_comments: (ast) => {
|
|
@@ -1563,14 +1717,41 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1563
1717
|
|
|
1564
1718
|
comments = comments
|
|
1565
1719
|
.filter((comment) => comment.start >= index)
|
|
1566
|
-
.map(({ type, value, start, end, loc }) => ({ type, value, start, end, loc }));
|
|
1720
|
+
.map(({ type, value, start, end, loc, context }) => ({ type, value, start, end, loc, context }));
|
|
1567
1721
|
|
|
1568
1722
|
walk(ast, null, {
|
|
1569
1723
|
_(node, { next, path }) {
|
|
1570
1724
|
let comment;
|
|
1571
1725
|
|
|
1726
|
+
const metadata = /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (node?.metadata);
|
|
1727
|
+
|
|
1728
|
+
if (metadata && metadata.commentContainerId !== undefined) {
|
|
1729
|
+
while (
|
|
1730
|
+
comments[0] &&
|
|
1731
|
+
comments[0].context &&
|
|
1732
|
+
comments[0].context.containerId === metadata.commentContainerId &&
|
|
1733
|
+
comments[0].context.beforeMeaningfulChild
|
|
1734
|
+
) {
|
|
1735
|
+
const elementComment = /** @type {CommentWithLocation & { context?: any }} */ (comments.shift());
|
|
1736
|
+
(metadata.elementLeadingComments ||= []).push(elementComment);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1572
1740
|
while (comments[0] && comments[0].start < node.start) {
|
|
1573
1741
|
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
|
1742
|
+
if (comment.loc) {
|
|
1743
|
+
const ancestorElements = path
|
|
1744
|
+
.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
|
|
1745
|
+
.sort((a, b) => a.loc.start.line - b.loc.start.line);
|
|
1746
|
+
|
|
1747
|
+
const targetAncestor = ancestorElements.find((ancestor) => comment.loc.start.line < ancestor.loc.start.line);
|
|
1748
|
+
|
|
1749
|
+
if (targetAncestor) {
|
|
1750
|
+
targetAncestor.metadata ??= {};
|
|
1751
|
+
(targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1574
1755
|
(node.leadingComments ||= []).push(comment);
|
|
1575
1756
|
}
|
|
1576
1757
|
|
|
@@ -1584,6 +1765,14 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1584
1765
|
return;
|
|
1585
1766
|
}
|
|
1586
1767
|
}
|
|
1768
|
+
// Handle empty Element nodes the same way as empty BlockStatements
|
|
1769
|
+
if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
|
|
1770
|
+
if (comments[0].start < node.end && comments[0].end < node.end) {
|
|
1771
|
+
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
|
1772
|
+
(node.innerComments ||= []).push(comment);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1587
1776
|
const parent = /** @type {any} */ (path.at(-1));
|
|
1588
1777
|
|
|
1589
1778
|
if (parent === undefined || node.end !== parent.end) {
|
|
@@ -575,13 +575,17 @@ const visitors = {
|
|
|
575
575
|
},
|
|
576
576
|
|
|
577
577
|
JSXElement(node, context) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
node,
|
|
583
|
-
);
|
|
578
|
+
const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat');
|
|
579
|
+
|
|
580
|
+
if (inside_tsx_compat) {
|
|
581
|
+
return context.next();
|
|
584
582
|
}
|
|
583
|
+
error(
|
|
584
|
+
'Elements cannot be used as generic expressions, only as statements within a component',
|
|
585
|
+
context.state.analysis.module.filename,
|
|
586
|
+
node,
|
|
587
|
+
);
|
|
588
|
+
|
|
585
589
|
},
|
|
586
590
|
|
|
587
591
|
Element(node, context) {
|
|
@@ -150,6 +150,21 @@ function visit_title_element(node, context) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* @param {string} name
|
|
155
|
+
* @param {any} context
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function import_from_ripple_if_needed(name, context) {
|
|
159
|
+
const alias = context.state.ripple_user_imports?.get?.(name);
|
|
160
|
+
|
|
161
|
+
if (!alias && !context.state.imports.has(`import { ${name} } from 'ripple'`)) {
|
|
162
|
+
context.state.imports.add(`import { ${name} } from 'ripple'`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return alias ?? name;
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
const visitors = {
|
|
154
169
|
_: function set_scope(node, { next, state }) {
|
|
155
170
|
const scope = state.scopes.get(node);
|
|
@@ -369,12 +384,10 @@ const visitors = {
|
|
|
369
384
|
|
|
370
385
|
TrackedArrayExpression(node, context) {
|
|
371
386
|
if (context.state.to_ts) {
|
|
372
|
-
|
|
373
|
-
context.state.imports.add(`import { TrackedArray } from 'ripple'`);
|
|
374
|
-
}
|
|
387
|
+
const arrayAlias = import_from_ripple_if_needed("TrackedArray", context);
|
|
375
388
|
|
|
376
389
|
return b.call(
|
|
377
|
-
b.member(b.id(
|
|
390
|
+
b.member(b.id(arrayAlias), b.id('from')),
|
|
378
391
|
b.array(node.elements.map((el) => context.visit(el))),
|
|
379
392
|
);
|
|
380
393
|
}
|
|
@@ -388,12 +401,10 @@ const visitors = {
|
|
|
388
401
|
|
|
389
402
|
TrackedObjectExpression(node, context) {
|
|
390
403
|
if (context.state.to_ts) {
|
|
391
|
-
|
|
392
|
-
context.state.imports.add(`import { TrackedObject } from 'ripple'`);
|
|
393
|
-
}
|
|
404
|
+
const objectAlias = import_from_ripple_if_needed("TrackedObject", context);
|
|
394
405
|
|
|
395
406
|
return b.new(
|
|
396
|
-
b.id(
|
|
407
|
+
b.id(objectAlias),
|
|
397
408
|
b.object(node.properties.map((prop) => context.visit(prop))),
|
|
398
409
|
);
|
|
399
410
|
}
|
|
@@ -407,11 +418,9 @@ const visitors = {
|
|
|
407
418
|
|
|
408
419
|
TrackedMapExpression(node, context) {
|
|
409
420
|
if (context.state.to_ts) {
|
|
410
|
-
|
|
411
|
-
context.state.imports.add(`import { TrackedMap } from 'ripple'`);
|
|
412
|
-
}
|
|
421
|
+
const mapAlias = import_from_ripple_if_needed('TrackedMap', context);
|
|
413
422
|
|
|
414
|
-
const calleeId = b.id(
|
|
423
|
+
const calleeId = b.id(mapAlias);
|
|
415
424
|
// Preserve location from original node for Volar mapping
|
|
416
425
|
calleeId.loc = node.loc;
|
|
417
426
|
// Add metadata for Volar mapping - map "TrackedMap" identifier to "#Map" in source
|
|
@@ -428,11 +437,9 @@ const visitors = {
|
|
|
428
437
|
|
|
429
438
|
TrackedSetExpression(node, context) {
|
|
430
439
|
if (context.state.to_ts) {
|
|
431
|
-
|
|
432
|
-
context.state.imports.add(`import { TrackedSet } from 'ripple'`);
|
|
433
|
-
}
|
|
440
|
+
const setAlias = import_from_ripple_if_needed('TrackedSet', context);
|
|
434
441
|
|
|
435
|
-
const calleeId = b.id(
|
|
442
|
+
const calleeId = b.id(setAlias);
|
|
436
443
|
// Preserve location from original node for Volar mapping
|
|
437
444
|
calleeId.loc = node.loc;
|
|
438
445
|
// Add metadata for Volar mapping - map "TrackedSet" identifier to "#Set" in source
|
|
@@ -541,6 +548,84 @@ const visitors = {
|
|
|
541
548
|
return visit_function(node, context);
|
|
542
549
|
},
|
|
543
550
|
|
|
551
|
+
JSXText(node, context) {
|
|
552
|
+
return b.literal(node.value + '');
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
JSXIdentifier(node, context) {
|
|
556
|
+
return b.id(node.name);
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
JSXElement(node, context) {
|
|
560
|
+
const name = node.openingElement.name;
|
|
561
|
+
const attributes = node.openingElement.attributes;
|
|
562
|
+
const normalized_children = node.children.filter((child) => {
|
|
563
|
+
return child.type !== 'JSXText' || child.value.trim() !== '';
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const props = b.object(
|
|
567
|
+
attributes.map((attr) => {
|
|
568
|
+
if (attr.type === 'JSXAttribute') {
|
|
569
|
+
return b.prop('init', context.visit(attr.name), context.visit(attr.value));
|
|
570
|
+
} else if (attr.type === 'JSXSpreadAttribute') {
|
|
571
|
+
return b.spread(context.visit(attr.argument));
|
|
572
|
+
}
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (normalize_children.length > 0) {
|
|
577
|
+
props.properties.push(
|
|
578
|
+
b.prop(
|
|
579
|
+
'init',
|
|
580
|
+
b.id('children'),
|
|
581
|
+
normalized_children.length === 1
|
|
582
|
+
? context.visit(normalized_children[0])
|
|
583
|
+
: b.array(normalized_children.map((child) => context.visit(child))),
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return b.call(
|
|
589
|
+
'_$_jsx',
|
|
590
|
+
name.type === 'JSXIdentifier' && name.name[0].toLowerCase() === name.name[0]
|
|
591
|
+
? b.literal(name.name)
|
|
592
|
+
: context.visit(name),
|
|
593
|
+
props,
|
|
594
|
+
);
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
TsxCompat(node, context) {
|
|
598
|
+
const { state, visit } = context;
|
|
599
|
+
|
|
600
|
+
state.template.push('<!>');
|
|
601
|
+
|
|
602
|
+
const normalized_children = node.children.filter((child) => {
|
|
603
|
+
return child.type !== 'JSXText' || child.value.trim() !== '';
|
|
604
|
+
});
|
|
605
|
+
const needs_fragment = normalized_children.length !== 1;
|
|
606
|
+
const id = state.flush_node();
|
|
607
|
+
const children_fn = b.arrow(
|
|
608
|
+
[b.id('__compat')],
|
|
609
|
+
needs_fragment
|
|
610
|
+
? b.call(
|
|
611
|
+
'__compat._jsxs',
|
|
612
|
+
b.id('__compat.Fragment'),
|
|
613
|
+
b.object([
|
|
614
|
+
b.prop(
|
|
615
|
+
'init',
|
|
616
|
+
b.id('children'),
|
|
617
|
+
b.array(normalized_children.map((child) => visit(child, state))),
|
|
618
|
+
),
|
|
619
|
+
]),
|
|
620
|
+
)
|
|
621
|
+
: visit(normalized_children[0], state),
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
context.state.init.push(
|
|
625
|
+
b.stmt(b.call('_$_.tsx_compat', b.literal(node.kind), id, children_fn)),
|
|
626
|
+
);
|
|
627
|
+
},
|
|
628
|
+
|
|
544
629
|
Element(node, context) {
|
|
545
630
|
const { state, visit } = context;
|
|
546
631
|
|
|
@@ -568,9 +653,10 @@ const visitors = {
|
|
|
568
653
|
|
|
569
654
|
const handle_static_attr = (name, value) => {
|
|
570
655
|
const attr_value = b.literal(
|
|
571
|
-
` ${name}${
|
|
572
|
-
|
|
573
|
-
|
|
656
|
+
` ${name}${
|
|
657
|
+
is_boolean_attribute(name) && value === true
|
|
658
|
+
? ''
|
|
659
|
+
: `="${value === true ? '' : escape_html(value, true)}"`
|
|
574
660
|
}`,
|
|
575
661
|
);
|
|
576
662
|
|
|
@@ -903,8 +989,11 @@ const visitors = {
|
|
|
903
989
|
}
|
|
904
990
|
|
|
905
991
|
if (node.metadata.scoped && state.component.css) {
|
|
906
|
-
const hasClassAttr = node.attributes.some(
|
|
907
|
-
attr
|
|
992
|
+
const hasClassAttr = node.attributes.some(
|
|
993
|
+
(attr) =>
|
|
994
|
+
attr.type === 'Attribute' &&
|
|
995
|
+
attr.name.type === 'Identifier' &&
|
|
996
|
+
attr.name.name === 'class',
|
|
908
997
|
);
|
|
909
998
|
if (!hasClassAttr) {
|
|
910
999
|
const name = is_spreading ? '#class' : 'class';
|
|
@@ -1061,10 +1150,10 @@ const visitors = {
|
|
|
1061
1150
|
operator === '='
|
|
1062
1151
|
? context.visit(right)
|
|
1063
1152
|
: b.binary(
|
|
1064
|
-
|
|
1065
|
-
/** @type {Expression} */(context.visit(left)),
|
|
1066
|
-
/** @type {Expression} */(context.visit(right)),
|
|
1067
|
-
|
|
1153
|
+
operator === '+=' ? '+' : operator === '-=' ? '-' : operator === '*=' ? '*' : '/',
|
|
1154
|
+
/** @type {Expression} */ (context.visit(left)),
|
|
1155
|
+
/** @type {Expression} */ (context.visit(right)),
|
|
1156
|
+
),
|
|
1068
1157
|
b.id('__block'),
|
|
1069
1158
|
);
|
|
1070
1159
|
}
|
|
@@ -1080,12 +1169,12 @@ const visitors = {
|
|
|
1080
1169
|
operator === '='
|
|
1081
1170
|
? context.visit(right)
|
|
1082
1171
|
: b.binary(
|
|
1083
|
-
|
|
1084
|
-
/** @type {Expression} */(
|
|
1085
|
-
|
|
1172
|
+
operator === '+=' ? '+' : operator === '-=' ? '-' : operator === '*=' ? '*' : '/',
|
|
1173
|
+
/** @type {Expression} */ (
|
|
1174
|
+
context.visit(left, { ...context.state, metadata: { tracking: false } })
|
|
1175
|
+
),
|
|
1176
|
+
/** @type {Expression} */ (context.visit(right)),
|
|
1086
1177
|
),
|
|
1087
|
-
/** @type {Expression} */(context.visit(right)),
|
|
1088
|
-
),
|
|
1089
1178
|
b.id('__block'),
|
|
1090
1179
|
);
|
|
1091
1180
|
}
|
|
@@ -1292,12 +1381,12 @@ const visitors = {
|
|
|
1292
1381
|
b.stmt(b.call(b.id('__render'), b.id(consequent_id))),
|
|
1293
1382
|
alternate_id
|
|
1294
1383
|
? b.stmt(
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1384
|
+
b.call(
|
|
1385
|
+
b.id('__render'),
|
|
1386
|
+
b.id(alternate_id),
|
|
1387
|
+
node.alternate ? b.literal(false) : undefined,
|
|
1388
|
+
),
|
|
1389
|
+
)
|
|
1301
1390
|
: undefined,
|
|
1302
1391
|
),
|
|
1303
1392
|
]),
|
|
@@ -1374,9 +1463,9 @@ const visitors = {
|
|
|
1374
1463
|
node.handler === null
|
|
1375
1464
|
? b.literal(null)
|
|
1376
1465
|
: b.arrow(
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1466
|
+
[b.id('__anchor'), ...(node.handler.param ? [node.handler.param] : [])],
|
|
1467
|
+
b.block(transform_body(node.handler.body.body, context)),
|
|
1468
|
+
),
|
|
1380
1469
|
node.pending === null
|
|
1381
1470
|
? undefined
|
|
1382
1471
|
: b.arrow([b.id('__anchor')], b.block(transform_body(node.pending.body, context))),
|
|
@@ -1502,7 +1591,7 @@ function join_template(items) {
|
|
|
1502
1591
|
}
|
|
1503
1592
|
|
|
1504
1593
|
for (const quasi of template.quasis) {
|
|
1505
|
-
quasi.value.raw = sanitize_template_string(/** @type {string} */(quasi.value.cooked));
|
|
1594
|
+
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
|
|
1506
1595
|
}
|
|
1507
1596
|
|
|
1508
1597
|
quasi.tail = true;
|
|
@@ -1555,12 +1644,12 @@ function transform_ts_child(node, context) {
|
|
|
1555
1644
|
const argument = visit(attr.argument, { ...state, metadata });
|
|
1556
1645
|
return b.jsx_spread_attribute(argument);
|
|
1557
1646
|
} else if (attr.type === 'RefAttribute') {
|
|
1558
|
-
|
|
1559
|
-
context.state.imports.add(`import { createRefKey } from 'ripple'`);
|
|
1560
|
-
}
|
|
1647
|
+
const createRefKeyAlias = import_from_ripple_if_needed('createRefKey', context);
|
|
1561
1648
|
const metadata = { await: false };
|
|
1562
1649
|
const argument = visit(attr.argument, { ...state, metadata });
|
|
1563
|
-
const wrapper = b.object(
|
|
1650
|
+
const wrapper = b.object(
|
|
1651
|
+
[b.prop('init', b.call(createRefKeyAlias), argument, true)]
|
|
1652
|
+
);
|
|
1564
1653
|
return b.jsx_spread_attribute(wrapper);
|
|
1565
1654
|
}
|
|
1566
1655
|
});
|
|
@@ -1841,6 +1930,8 @@ function transform_children(children, context) {
|
|
|
1841
1930
|
visit(node, { ...state, flush_node, namespace: state.namespace });
|
|
1842
1931
|
} else if (node.type === 'HeadElement') {
|
|
1843
1932
|
visit(node, { ...state, flush_node, namespace: state.namespace });
|
|
1933
|
+
} else if (node.type === 'TsxCompat') {
|
|
1934
|
+
visit(node, { ...state, flush_node, namespace: state.namespace });
|
|
1844
1935
|
} else if (node.type === 'Html') {
|
|
1845
1936
|
const metadata = { tracking: false, await: false };
|
|
1846
1937
|
const expression = visit(node.expression, { ...state, metadata });
|
|
@@ -1966,9 +2057,75 @@ function transform_body(body, { visit, state }) {
|
|
|
1966
2057
|
return [...body_state.setup, ...body_state.init, ...body_state.final];
|
|
1967
2058
|
}
|
|
1968
2059
|
|
|
2060
|
+
/**
|
|
2061
|
+
* Create a TSX language handler with enhanced TypeScript support
|
|
2062
|
+
* @returns {Object} TSX language handler with TypeScript return type support
|
|
2063
|
+
*/
|
|
2064
|
+
function create_tsx_with_typescript_support() {
|
|
2065
|
+
const base_tsx = tsx();
|
|
2066
|
+
|
|
2067
|
+
// Override the ArrowFunctionExpression handler to support TypeScript return types
|
|
2068
|
+
return {
|
|
2069
|
+
...base_tsx,
|
|
2070
|
+
ArrowFunctionExpression(node, context) {
|
|
2071
|
+
if (node.async) context.write('async ');
|
|
2072
|
+
|
|
2073
|
+
context.write('(');
|
|
2074
|
+
// Visit each parameter
|
|
2075
|
+
for (let i = 0; i < node.params.length; i++) {
|
|
2076
|
+
if (i > 0) context.write(', ');
|
|
2077
|
+
context.visit(node.params[i]);
|
|
2078
|
+
}
|
|
2079
|
+
context.write(')');
|
|
2080
|
+
|
|
2081
|
+
// Add TypeScript return type annotation if present
|
|
2082
|
+
if (node.returnType) {
|
|
2083
|
+
context.visit(node.returnType);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
context.write(' => ');
|
|
2087
|
+
|
|
2088
|
+
if (
|
|
2089
|
+
node.body.type === 'ObjectExpression' ||
|
|
2090
|
+
(node.body.type === 'AssignmentExpression' && node.body.left.type === 'ObjectPattern') ||
|
|
2091
|
+
(node.body.type === 'LogicalExpression' && node.body.left.type === 'ObjectExpression') ||
|
|
2092
|
+
(node.body.type === 'ConditionalExpression' && node.body.test.type === 'ObjectExpression')
|
|
2093
|
+
) {
|
|
2094
|
+
context.write('(');
|
|
2095
|
+
context.visit(node.body);
|
|
2096
|
+
context.write(')');
|
|
2097
|
+
} else {
|
|
2098
|
+
context.visit(node.body);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1969
2104
|
export function transform_client(filename, source, analysis, to_ts) {
|
|
2105
|
+
/**
|
|
2106
|
+
* User's named imports from 'ripple' so we can reuse them in TS output
|
|
2107
|
+
* when transforming shorthand syntax. E.g., if the user has already imported
|
|
2108
|
+
* TrackedArray, we want to reuse that import instead of importing it again
|
|
2109
|
+
* if we encounter `#[]`. It's a Map of export name to local name in case the
|
|
2110
|
+
* user renamed something when importing.
|
|
2111
|
+
* @type {Map<string, string>}
|
|
2112
|
+
*/
|
|
2113
|
+
const ripple_user_imports = new Map(); // exported -> local
|
|
2114
|
+
if (analysis && analysis.ast && Array.isArray(analysis.ast.body)) {
|
|
2115
|
+
for (const stmt of analysis.ast.body) {
|
|
2116
|
+
if (stmt && stmt.type === 'ImportDeclaration' && stmt.source && stmt.source.value === 'ripple') {
|
|
2117
|
+
for (const spec of stmt.specifiers || []) {
|
|
2118
|
+
if (spec.type === 'ImportSpecifier' && spec.imported && spec.local) {
|
|
2119
|
+
ripple_user_imports.set(spec.imported.name, spec.local.name);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1970
2126
|
const state = {
|
|
1971
2127
|
imports: new Set(),
|
|
2128
|
+
ripple_user_imports,
|
|
1972
2129
|
events: new Set(),
|
|
1973
2130
|
template: null,
|
|
1974
2131
|
hoisted: [],
|
|
@@ -1985,7 +2142,7 @@ export function transform_client(filename, source, analysis, to_ts) {
|
|
|
1985
2142
|
};
|
|
1986
2143
|
|
|
1987
2144
|
const program = /** @type {Program} */ (
|
|
1988
|
-
walk(/** @type {Node} */(analysis.ast), { ...state, namespace: 'html' }, visitors)
|
|
2145
|
+
walk(/** @type {Node} */ (analysis.ast), { ...state, namespace: 'html' }, visitors)
|
|
1989
2146
|
);
|
|
1990
2147
|
|
|
1991
2148
|
for (const hoisted of state.hoisted) {
|
|
@@ -2004,7 +2161,7 @@ export function transform_client(filename, source, analysis, to_ts) {
|
|
|
2004
2161
|
);
|
|
2005
2162
|
}
|
|
2006
2163
|
|
|
2007
|
-
const js = print(program, tsx(), {
|
|
2164
|
+
const js = print(program, to_ts ? create_tsx_with_typescript_support() : tsx(), {
|
|
2008
2165
|
sourceMapContent: source,
|
|
2009
2166
|
sourceMapSource: path.basename(filename),
|
|
2010
2167
|
});
|
|
@@ -254,6 +254,16 @@ export function convert_source_map_to_mappings(ast, source, generated_code, sour
|
|
|
254
254
|
visit(node.local);
|
|
255
255
|
}
|
|
256
256
|
return;
|
|
257
|
+
} else if (node.type === 'ExportSpecifier') {
|
|
258
|
+
// If local and exported are the same, only visit local to avoid duplicates
|
|
259
|
+
// Otherwise visit both in order
|
|
260
|
+
if (node.local && node.exported && node.local.name !== node.exported.name) {
|
|
261
|
+
visit(node.local);
|
|
262
|
+
visit(node.exported);
|
|
263
|
+
} else if (node.local) {
|
|
264
|
+
visit(node.local);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
257
267
|
} else if (node.type === 'ExportNamedDeclaration') {
|
|
258
268
|
if (node.specifiers && node.specifiers.length > 0) {
|
|
259
269
|
for (const specifier of node.specifiers) {
|
|
@@ -119,6 +119,18 @@ export interface Element extends Omit<Node, 'type'> {
|
|
|
119
119
|
metadata: any;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* TSX compatibility node for elements with namespaces like <tsx:react>
|
|
124
|
+
* Note: TsxCompat elements cannot be self-closing and must have a closing tag
|
|
125
|
+
*/
|
|
126
|
+
export interface TsxCompat extends Omit<Node, 'type'> {
|
|
127
|
+
type: 'TsxCompat';
|
|
128
|
+
kind: string;
|
|
129
|
+
attributes: Array<Attribute | SpreadAttribute>;
|
|
130
|
+
children: Node[];
|
|
131
|
+
metadata: any;
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
/**
|
|
123
135
|
* Ripple attribute node
|
|
124
136
|
*/
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import { parse, compile } from 'ripple/compiler';
|
|
1
|
+
import { parse, compile, compile_to_volar_mappings } from 'ripple/compiler';
|
|
2
|
+
|
|
3
|
+
function count_occurrences(string, subString) {
|
|
4
|
+
let count = 0;
|
|
5
|
+
let pos = string.indexOf(subString);
|
|
6
|
+
|
|
7
|
+
while (pos !== -1) {
|
|
8
|
+
count++;
|
|
9
|
+
pos = string.indexOf(subString, pos + subString.length);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return count;
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
describe('compiler > basics', () => {
|
|
4
16
|
it('parses style content correctly', () => {
|
|
@@ -200,4 +212,108 @@ describe('compiler > basics', () => {
|
|
|
200
212
|
|
|
201
213
|
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
202
214
|
});
|
|
215
|
+
|
|
216
|
+
it("doesn't add duplicate imports when encountering shorthand syntax", () => {
|
|
217
|
+
const source = `
|
|
218
|
+
import {
|
|
219
|
+
TrackedArray,
|
|
220
|
+
TrackedObject,
|
|
221
|
+
TrackedSet,
|
|
222
|
+
TrackedMap,
|
|
223
|
+
createRefKey,
|
|
224
|
+
} from 'ripple';
|
|
225
|
+
|
|
226
|
+
component App() {
|
|
227
|
+
const items = #[1, 2, 3];
|
|
228
|
+
const obj = #{ a: 1, b: 2, c: 3 };
|
|
229
|
+
const set = #Set([1, 2, 3]);
|
|
230
|
+
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
231
|
+
|
|
232
|
+
<div {ref () => {}} />
|
|
233
|
+
}
|
|
234
|
+
`;
|
|
235
|
+
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
236
|
+
|
|
237
|
+
expect(count_occurrences(result, "TrackedArray")).toBe(2);
|
|
238
|
+
expect(count_occurrences(result, "TrackedObject")).toBe(2);
|
|
239
|
+
expect(count_occurrences(result, "TrackedSet")).toBe(2);
|
|
240
|
+
expect(count_occurrences(result, "TrackedMap")).toBe(2);
|
|
241
|
+
expect(count_occurrences(result, "createRefKey")).toBe(2);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("doesn't add duplicate imports for renamed imports when encountering shorthand syntax", () => {
|
|
245
|
+
const source = `
|
|
246
|
+
import {
|
|
247
|
+
TrackedArray as TA,
|
|
248
|
+
TrackedObject as TO,
|
|
249
|
+
TrackedSet as TS,
|
|
250
|
+
TrackedMap as TM,
|
|
251
|
+
createRefKey as crk,
|
|
252
|
+
} from 'ripple';
|
|
253
|
+
|
|
254
|
+
component App() {
|
|
255
|
+
const items = #[1, 2, 3];
|
|
256
|
+
const obj = #{ a: 1, b: 2, c: 3 };
|
|
257
|
+
const set = #Set([1, 2, 3]);
|
|
258
|
+
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
259
|
+
|
|
260
|
+
<div {ref () => {}} />
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
264
|
+
|
|
265
|
+
expect(count_occurrences(result, "TrackedArray")).toBe(1);
|
|
266
|
+
expect(count_occurrences(result, "TA")).toBe(2);
|
|
267
|
+
expect(count_occurrences(result, "TrackedObject")).toBe(1);
|
|
268
|
+
expect(count_occurrences(result, "TO")).toBe(2);
|
|
269
|
+
expect(count_occurrences(result, "TrackedSet")).toBe(1);
|
|
270
|
+
expect(count_occurrences(result, "TS")).toBe(2);
|
|
271
|
+
expect(count_occurrences(result, "TrackedMap")).toBe(1);
|
|
272
|
+
expect(count_occurrences(result, "TM")).toBe(2);
|
|
273
|
+
expect(count_occurrences(result, "createRefKey")).toBe(1);
|
|
274
|
+
expect(count_occurrences(result, "crk")).toBe(2);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('adds missing imports for shorthand syntax', () => {
|
|
278
|
+
const source = `
|
|
279
|
+
component App() {
|
|
280
|
+
const items = #[1, 2, 3];
|
|
281
|
+
const obj = #{ a: 1, b: 2, c: 3 };
|
|
282
|
+
const set = #Set([1, 2, 3]);
|
|
283
|
+
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
284
|
+
|
|
285
|
+
<div {ref () => {}} />
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
289
|
+
|
|
290
|
+
expect(count_occurrences(result, "TrackedArray")).toBe(2);
|
|
291
|
+
expect(count_occurrences(result, "TrackedObject")).toBe(2);
|
|
292
|
+
expect(count_occurrences(result, "TrackedSet")).toBe(2);
|
|
293
|
+
expect(count_occurrences(result, "TrackedMap")).toBe(2);
|
|
294
|
+
expect(count_occurrences(result, "createRefKey")).toBe(2);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('only adds missing imports for shorthand syntax, reusing existing ones', () => {
|
|
298
|
+
const source = `
|
|
299
|
+
import { TrackedArray, TrackedMap, createRefKey as crk } from 'ripple';
|
|
300
|
+
|
|
301
|
+
component App() {
|
|
302
|
+
const items = #[1, 2, 3];
|
|
303
|
+
const obj = #{ a: 1, b: 2, c: 3 };
|
|
304
|
+
const set = #Set([1, 2, 3]);
|
|
305
|
+
const map = #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
306
|
+
|
|
307
|
+
<div {ref () => {}} />
|
|
308
|
+
}
|
|
309
|
+
`;
|
|
310
|
+
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
311
|
+
|
|
312
|
+
expect(count_occurrences(result, "TrackedArray")).toBe(2);
|
|
313
|
+
expect(count_occurrences(result, "TrackedObject")).toBe(2);
|
|
314
|
+
expect(count_occurrences(result, "TrackedSet")).toBe(2);
|
|
315
|
+
expect(count_occurrences(result, "TrackedMap")).toBe(2);
|
|
316
|
+
expect(count_occurrences(result, "createRefKey")).toBe(1);
|
|
317
|
+
expect(count_occurrences(result, "crk")).toBe(2);
|
|
318
|
+
});
|
|
203
319
|
});
|