ripple 0.2.130 → 0.2.132
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 +133 -25
- 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 +28 -2
- 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.132",
|
|
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.132"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -107,7 +107,8 @@ function RipplePlugin(config) {
|
|
|
107
107
|
// Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
|
|
108
108
|
// At component top-level (scopeStack.length <= 4), apply JSX detection logic
|
|
109
109
|
// BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
|
|
110
|
-
const nextChar =
|
|
110
|
+
const nextChar =
|
|
111
|
+
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
111
112
|
const isClosingTag = nextChar === 47; // '/'
|
|
112
113
|
|
|
113
114
|
if (this.scopeStack.length >= 5 && !isClosingTag) {
|
|
@@ -169,15 +170,19 @@ function RipplePlugin(config) {
|
|
|
169
170
|
|
|
170
171
|
// Check if this is #Map or #Set
|
|
171
172
|
if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
|
|
172
|
-
const charAfter =
|
|
173
|
-
|
|
173
|
+
const charAfter =
|
|
174
|
+
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
175
|
+
if (charAfter === 40) {
|
|
176
|
+
// ( character
|
|
174
177
|
this.pos += 4; // consume '#Map'
|
|
175
178
|
return this.finishToken(tt.name, '#Map');
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
|
|
179
|
-
const charAfter =
|
|
180
|
-
|
|
182
|
+
const charAfter =
|
|
183
|
+
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
184
|
+
if (charAfter === 40) {
|
|
185
|
+
// ( character
|
|
181
186
|
this.pos += 4; // consume '#Set'
|
|
182
187
|
return this.finishToken(tt.name, '#Set');
|
|
183
188
|
}
|
|
@@ -206,12 +211,18 @@ function RipplePlugin(config) {
|
|
|
206
211
|
// Check if this is an invalid #Identifier pattern
|
|
207
212
|
// Valid patterns: #[, #{, #Map(, #Set(, #server
|
|
208
213
|
// If we see # followed by an uppercase letter that isn't Map or Set, it's an error
|
|
209
|
-
if (nextChar >= 65 && nextChar <= 90) {
|
|
214
|
+
if (nextChar >= 65 && nextChar <= 90) {
|
|
215
|
+
// A-Z
|
|
210
216
|
// Extract the identifier name
|
|
211
217
|
let identEnd = this.pos + 1;
|
|
212
218
|
while (identEnd < this.input.length) {
|
|
213
219
|
const ch = this.input.charCodeAt(identEnd);
|
|
214
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
(ch >= 65 && ch <= 90) ||
|
|
222
|
+
(ch >= 97 && ch <= 122) ||
|
|
223
|
+
(ch >= 48 && ch <= 57) ||
|
|
224
|
+
ch === 95
|
|
225
|
+
) {
|
|
215
226
|
// A-Z, a-z, 0-9, _
|
|
216
227
|
identEnd++;
|
|
217
228
|
} else {
|
|
@@ -220,7 +231,10 @@ function RipplePlugin(config) {
|
|
|
220
231
|
}
|
|
221
232
|
const identName = this.input.slice(this.pos + 1, identEnd);
|
|
222
233
|
if (identName !== 'Map' && identName !== 'Set') {
|
|
223
|
-
this.raise(
|
|
234
|
+
this.raise(
|
|
235
|
+
this.pos,
|
|
236
|
+
`Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
|
|
237
|
+
);
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
240
|
}
|
|
@@ -940,9 +954,13 @@ function RipplePlugin(config) {
|
|
|
940
954
|
return this.finishNode(node, 'JSXIdentifier');
|
|
941
955
|
}
|
|
942
956
|
|
|
943
|
-
// Override jsx_parseElementName to support @ syntax in member expressions
|
|
944
957
|
jsx_parseElementName() {
|
|
945
|
-
let node = this.
|
|
958
|
+
let node = this.jsx_parseNamespacedName();
|
|
959
|
+
|
|
960
|
+
if (node.type === 'JSXNamespacedName') {
|
|
961
|
+
return node;
|
|
962
|
+
}
|
|
963
|
+
|
|
946
964
|
if (this.eat(tt.dot)) {
|
|
947
965
|
let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
|
|
948
966
|
memberExpr.object = node;
|
|
@@ -1019,11 +1037,15 @@ function RipplePlugin(config) {
|
|
|
1019
1037
|
}
|
|
1020
1038
|
|
|
1021
1039
|
jsx_readToken() {
|
|
1040
|
+
const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
|
|
1041
|
+
if (inside_tsx_compat) {
|
|
1042
|
+
return super.jsx_readToken();
|
|
1043
|
+
}
|
|
1022
1044
|
let out = '',
|
|
1023
1045
|
chunkStart = this.pos;
|
|
1024
1046
|
const tok = this.acornTypeScript.tokTypes;
|
|
1025
1047
|
|
|
1026
|
-
|
|
1048
|
+
while (true) {
|
|
1027
1049
|
if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
|
|
1028
1050
|
let ch = this.input.charCodeAt(this.pos);
|
|
1029
1051
|
|
|
@@ -1178,10 +1200,29 @@ function RipplePlugin(config) {
|
|
|
1178
1200
|
element.start = position.index;
|
|
1179
1201
|
element.loc.start = position;
|
|
1180
1202
|
element.metadata = {};
|
|
1181
|
-
element.type = 'Element';
|
|
1182
|
-
this.#path.push(element);
|
|
1183
1203
|
element.children = [];
|
|
1184
1204
|
const open = this.jsx_parseOpeningElementAt();
|
|
1205
|
+
|
|
1206
|
+
// Check if this is a namespaced element (tsx:react)
|
|
1207
|
+
const is_tsx_compat = open.name.type === 'JSXNamespacedName';
|
|
1208
|
+
|
|
1209
|
+
if (is_tsx_compat) {
|
|
1210
|
+
element.type = 'TsxCompat';
|
|
1211
|
+
element.kind = open.name.name.name; // e.g., "react" from "tsx:react"
|
|
1212
|
+
|
|
1213
|
+
if (open.selfClosing) {
|
|
1214
|
+
const tagName = open.name.namespace.name + ':' + open.name.name.name;
|
|
1215
|
+
this.raise(
|
|
1216
|
+
open.start,
|
|
1217
|
+
`TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
element.type = 'Element';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
this.#path.push(element);
|
|
1225
|
+
|
|
1185
1226
|
for (const attr of open.attributes) {
|
|
1186
1227
|
if (attr.type === 'JSXAttribute') {
|
|
1187
1228
|
attr.type = 'Attribute';
|
|
@@ -1195,13 +1236,16 @@ function RipplePlugin(config) {
|
|
|
1195
1236
|
}
|
|
1196
1237
|
}
|
|
1197
1238
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1239
|
+
|
|
1240
|
+
if (!is_tsx_compat) {
|
|
1241
|
+
if (open.name.type === 'JSXIdentifier') {
|
|
1242
|
+
open.name.type = 'Identifier';
|
|
1243
|
+
}
|
|
1244
|
+
element.id = convert_from_jsx(open.name);
|
|
1245
|
+
element.selfClosing = open.selfClosing;
|
|
1200
1246
|
}
|
|
1201
1247
|
|
|
1202
|
-
element.id = convert_from_jsx(open.name);
|
|
1203
1248
|
element.attributes = open.attributes;
|
|
1204
|
-
element.selfClosing = open.selfClosing;
|
|
1205
1249
|
element.metadata = {};
|
|
1206
1250
|
|
|
1207
1251
|
if (element.selfClosing) {
|
|
@@ -1295,10 +1339,40 @@ function RipplePlugin(config) {
|
|
|
1295
1339
|
this.parseTemplateBody(element.children);
|
|
1296
1340
|
this.exitScope();
|
|
1297
1341
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1342
|
+
if (element.type === 'TsxCompat') {
|
|
1343
|
+
this.#path.pop();
|
|
1344
|
+
|
|
1345
|
+
const raise_error = () => {
|
|
1346
|
+
this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
this.next();
|
|
1350
|
+
// we should expect to see </tsx:kind>
|
|
1351
|
+
if (this.value !== '/') {
|
|
1352
|
+
raise_error();
|
|
1353
|
+
}
|
|
1354
|
+
this.next();
|
|
1355
|
+
if (this.value !== 'tsx') {
|
|
1356
|
+
raise_error();
|
|
1357
|
+
}
|
|
1358
|
+
this.next();
|
|
1359
|
+
if (this.type.label !== ':') {
|
|
1360
|
+
raise_error();
|
|
1361
|
+
}
|
|
1362
|
+
this.next();
|
|
1363
|
+
if (this.value !== element.kind) {
|
|
1364
|
+
raise_error();
|
|
1365
|
+
}
|
|
1366
|
+
this.next();
|
|
1367
|
+
if (this.type.label !== 'jsxTagEnd') {
|
|
1368
|
+
raise_error();
|
|
1369
|
+
}
|
|
1370
|
+
this.next();
|
|
1371
|
+
} else if (this.#path[this.#path.length - 1] === element) {
|
|
1372
|
+
// Check if this element was properly closed
|
|
1373
|
+
// If we reach here and this element is still in the path, it means it was never closed
|
|
1301
1374
|
const tagName = this.getElementName(element.id);
|
|
1375
|
+
|
|
1302
1376
|
this.raise(
|
|
1303
1377
|
this.start,
|
|
1304
1378
|
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
@@ -1314,13 +1388,14 @@ function RipplePlugin(config) {
|
|
|
1314
1388
|
}
|
|
1315
1389
|
}
|
|
1316
1390
|
|
|
1317
|
-
this.finishNode(element,
|
|
1391
|
+
this.finishNode(element, element.type);
|
|
1318
1392
|
return element;
|
|
1319
1393
|
}
|
|
1320
1394
|
|
|
1321
1395
|
parseTemplateBody(body) {
|
|
1322
|
-
|
|
1396
|
+
const inside_func =
|
|
1323
1397
|
this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
|
|
1398
|
+
const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
|
|
1324
1399
|
|
|
1325
1400
|
if (!inside_func) {
|
|
1326
1401
|
if (this.type.label === 'return') {
|
|
@@ -1334,6 +1409,19 @@ function RipplePlugin(config) {
|
|
|
1334
1409
|
}
|
|
1335
1410
|
}
|
|
1336
1411
|
|
|
1412
|
+
if (inside_tsx_compat) {
|
|
1413
|
+
this.exprAllowed = true;
|
|
1414
|
+
|
|
1415
|
+
while (true) {
|
|
1416
|
+
const node = super.parseExpression();
|
|
1417
|
+
body.push(node);
|
|
1418
|
+
|
|
1419
|
+
if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1337
1425
|
if (this.type.label === '{') {
|
|
1338
1426
|
const node = this.jsx_parseExpressionContainer();
|
|
1339
1427
|
node.type = node.html ? 'Html' : 'Text';
|
|
@@ -1350,12 +1438,32 @@ function RipplePlugin(config) {
|
|
|
1350
1438
|
|
|
1351
1439
|
// Validate that the closing tag matches the opening tag
|
|
1352
1440
|
const currentElement = this.#path[this.#path.length - 1];
|
|
1353
|
-
if (
|
|
1441
|
+
if (
|
|
1442
|
+
!currentElement ||
|
|
1443
|
+
(currentElement.type !== 'Element' && currentElement.type !== 'TsxCompat')
|
|
1444
|
+
) {
|
|
1354
1445
|
this.raise(this.start, 'Unexpected closing tag');
|
|
1355
1446
|
}
|
|
1356
1447
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1448
|
+
let openingTagName;
|
|
1449
|
+
let closingTagName;
|
|
1450
|
+
|
|
1451
|
+
if (currentElement.type === 'TsxCompat') {
|
|
1452
|
+
if (closingTag.type === 'JSXNamespacedName') {
|
|
1453
|
+
openingTagName = 'tsx:' + currentElement.kind;
|
|
1454
|
+
closingTagName = closingTag.namespace.name + ':' + closingTag.name.name;
|
|
1455
|
+
} else {
|
|
1456
|
+
openingTagName = 'tsx:' + currentElement.kind;
|
|
1457
|
+
closingTagName = this.getElementName(closingTag);
|
|
1458
|
+
}
|
|
1459
|
+
} else {
|
|
1460
|
+
// Regular Element node
|
|
1461
|
+
openingTagName = this.getElementName(currentElement.id);
|
|
1462
|
+
closingTagName =
|
|
1463
|
+
closingTag.type === 'JSXNamespacedName'
|
|
1464
|
+
? closingTag.namespace.name + ':' + closingTag.name.name
|
|
1465
|
+
: this.getElementName(closingTag);
|
|
1466
|
+
}
|
|
1359
1467
|
|
|
1360
1468
|
if (openingTagName !== closingTagName) {
|
|
1361
1469
|
this.raise(
|
|
@@ -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) {
|
|
@@ -366,9 +376,13 @@ export function convert_source_map_to_mappings(ast, source, generated_code, sour
|
|
|
366
376
|
}
|
|
367
377
|
return;
|
|
368
378
|
} else if (node.type === 'VariableDeclarator') {
|
|
369
|
-
// Visit in source order: id, init
|
|
379
|
+
// Visit in source order: id, typeAnnotation, init
|
|
370
380
|
if (node.id) {
|
|
371
381
|
visit(node.id);
|
|
382
|
+
// Visit type annotation if present
|
|
383
|
+
if (node.id.typeAnnotation) {
|
|
384
|
+
visit(node.id.typeAnnotation);
|
|
385
|
+
}
|
|
372
386
|
}
|
|
373
387
|
if (node.init) {
|
|
374
388
|
visit(node.init);
|
|
@@ -486,9 +500,13 @@ export function convert_source_map_to_mappings(ast, source, generated_code, sour
|
|
|
486
500
|
}
|
|
487
501
|
return;
|
|
488
502
|
} else if (node.type === 'AssignmentExpression' || node.type === 'AssignmentPattern') {
|
|
489
|
-
// Visit in source order: left, right
|
|
503
|
+
// Visit in source order: left, typeAnnotation, right
|
|
490
504
|
if (node.left) {
|
|
491
505
|
visit(node.left);
|
|
506
|
+
// Visit type annotation if present (for AssignmentPattern)
|
|
507
|
+
if (node.left.typeAnnotation) {
|
|
508
|
+
visit(node.left.typeAnnotation);
|
|
509
|
+
}
|
|
492
510
|
}
|
|
493
511
|
if (node.right) {
|
|
494
512
|
visit(node.right);
|
|
@@ -647,6 +665,14 @@ export function convert_source_map_to_mappings(ast, source, generated_code, sour
|
|
|
647
665
|
// Visit the argument
|
|
648
666
|
if (node.argument) {
|
|
649
667
|
visit(node.argument);
|
|
668
|
+
// Visit type annotation if present (for RestElement)
|
|
669
|
+
if (node.argument.typeAnnotation) {
|
|
670
|
+
visit(node.argument.typeAnnotation);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// RestElement itself can have typeAnnotation
|
|
674
|
+
if (node.typeAnnotation) {
|
|
675
|
+
visit(node.typeAnnotation);
|
|
650
676
|
}
|
|
651
677
|
return;
|
|
652
678
|
} else if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
|
|
@@ -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
|
});
|