ripple 0.2.101 → 0.2.103
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 +177 -6
- package/src/compiler/phases/3-transform/client/index.js +23 -17
- package/src/runtime/internal/client/for.js +2 -0
- package/tests/client/__snapshots__/computed-properties.test.ripple.snap +49 -0
- package/tests/client/composite.test.ripple +78 -75
- package/tests/client/computed-properties.test.ripple +64 -0
- package/tests/client/typescript-generics.test.ripple +140 -29
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.103",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
67
|
+
"@sveltejs/acorn-typescript": "^1.0.6",
|
|
67
68
|
"acorn": "^8.15.0",
|
|
68
|
-
"acorn-typescript": "^1.4.13",
|
|
69
69
|
"clsx": "^2.1.1",
|
|
70
70
|
"esrap": "^2.1.0",
|
|
71
71
|
"is-reference": "^3.0.3",
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* } from '#compiler' */
|
|
6
6
|
|
|
7
7
|
import * as acorn from 'acorn';
|
|
8
|
-
import { tsPlugin } from 'acorn-typescript';
|
|
8
|
+
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
9
9
|
import { parse_style } from './style.js';
|
|
10
10
|
import { walk } from 'zimmerframe';
|
|
11
11
|
import { regex_newline_characters } from '../../../utils/patterns.js';
|
|
12
12
|
|
|
13
|
-
const parser = acorn.Parser.extend(tsPlugin({
|
|
13
|
+
const parser = acorn.Parser.extend(tsPlugin({ jsx: true }), RipplePlugin());
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Convert JSX node types to regular JavaScript node types
|
|
@@ -67,7 +67,51 @@ function RipplePlugin(config) {
|
|
|
67
67
|
getTokenFromCode(code) {
|
|
68
68
|
if (code === 60) {
|
|
69
69
|
// < character
|
|
70
|
-
|
|
70
|
+
const inComponent = this.#path.findLast((n) => n.type === 'Component');
|
|
71
|
+
|
|
72
|
+
// Check if this could be TypeScript generics instead of JSX
|
|
73
|
+
// TypeScript generics appear after: identifiers, closing parens, 'new' keyword
|
|
74
|
+
// For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
|
|
75
|
+
// This check applies everywhere, not just inside components
|
|
76
|
+
|
|
77
|
+
// Look back to see what precedes the <
|
|
78
|
+
let lookback = this.pos - 1;
|
|
79
|
+
|
|
80
|
+
// Skip whitespace backwards
|
|
81
|
+
while (lookback >= 0) {
|
|
82
|
+
const ch = this.input.charCodeAt(lookback);
|
|
83
|
+
if (ch !== 32 && ch !== 9) break; // not space or tab
|
|
84
|
+
lookback--;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check what character/token precedes the <
|
|
88
|
+
if (lookback >= 0) {
|
|
89
|
+
const prevChar = this.input.charCodeAt(lookback);
|
|
90
|
+
|
|
91
|
+
// If preceded by identifier character (letter, digit, _, $) or closing paren,
|
|
92
|
+
// this is likely TypeScript generics, not JSX
|
|
93
|
+
const isIdentifierChar =
|
|
94
|
+
(prevChar >= 65 && prevChar <= 90) || // A-Z
|
|
95
|
+
(prevChar >= 97 && prevChar <= 122) || // a-z
|
|
96
|
+
(prevChar >= 48 && prevChar <= 57) || // 0-9
|
|
97
|
+
prevChar === 95 || // _
|
|
98
|
+
prevChar === 36 || // $
|
|
99
|
+
prevChar === 41; // )
|
|
100
|
+
|
|
101
|
+
if (isIdentifierChar) {
|
|
102
|
+
return super.getTokenFromCode(code);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (inComponent) {
|
|
107
|
+
// Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
|
|
108
|
+
// At component top-level (scopeStack.length <= 4), apply JSX detection logic
|
|
109
|
+
if (this.scopeStack.length >= 5) {
|
|
110
|
+
// Inside function - treat as TypeScript generic, not JSX
|
|
111
|
+
++this.pos;
|
|
112
|
+
return this.finishToken(tt.relational, '<');
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
// Check if everything before this position on the current line is whitespace
|
|
72
116
|
let lineStart = this.pos - 1;
|
|
73
117
|
while (
|
|
@@ -214,6 +258,88 @@ function RipplePlugin(config) {
|
|
|
214
258
|
return node;
|
|
215
259
|
}
|
|
216
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
|
|
263
|
+
* @param {any} base - The base expression
|
|
264
|
+
* @param {number} startPos - Start position
|
|
265
|
+
* @param {any} startLoc - Start location
|
|
266
|
+
* @param {boolean} noCalls - Whether calls are disallowed
|
|
267
|
+
* @param {any} maybeAsyncArrow - Optional async arrow flag
|
|
268
|
+
* @param {any} optionalChained - Optional chaining flag
|
|
269
|
+
* @param {any} forInit - For-init flag
|
|
270
|
+
* @returns {any} Parsed subscript expression
|
|
271
|
+
*/
|
|
272
|
+
parseSubscripts(
|
|
273
|
+
base,
|
|
274
|
+
startPos,
|
|
275
|
+
startLoc,
|
|
276
|
+
noCalls,
|
|
277
|
+
maybeAsyncArrow,
|
|
278
|
+
optionalChained,
|
|
279
|
+
forInit,
|
|
280
|
+
) {
|
|
281
|
+
// Check for `.@[` pattern for reactive computed member access
|
|
282
|
+
const isDotOrOptional = this.type === tt.dot || this.type === tt.questionDot;
|
|
283
|
+
|
|
284
|
+
if (isDotOrOptional) {
|
|
285
|
+
// Check the next two characters without consuming tokens
|
|
286
|
+
// this.pos currently points AFTER the dot token
|
|
287
|
+
const nextChar = this.input.charCodeAt(this.pos);
|
|
288
|
+
const charAfter = this.input.charCodeAt(this.pos + 1);
|
|
289
|
+
|
|
290
|
+
// Check for @[ pattern (@ = 64, [ = 91)
|
|
291
|
+
if (nextChar === 64 && charAfter === 91) {
|
|
292
|
+
const node = this.startNodeAt(startPos, startLoc);
|
|
293
|
+
node.object = base;
|
|
294
|
+
node.computed = true;
|
|
295
|
+
node.optional = this.type === tt.questionDot;
|
|
296
|
+
node.tracked = true;
|
|
297
|
+
|
|
298
|
+
// Consume the dot/questionDot token
|
|
299
|
+
this.next();
|
|
300
|
+
|
|
301
|
+
// Manually skip the @ character
|
|
302
|
+
this.pos += 1;
|
|
303
|
+
|
|
304
|
+
// Now call finishToken to properly consume the [ bracket
|
|
305
|
+
this.finishToken(tt.bracketL);
|
|
306
|
+
|
|
307
|
+
// Now we're positioned correctly to parse the expression
|
|
308
|
+
this.next(); // Move to first token inside brackets
|
|
309
|
+
|
|
310
|
+
// Parse the expression inside brackets
|
|
311
|
+
node.property = this.parseExpression();
|
|
312
|
+
|
|
313
|
+
// Expect closing bracket
|
|
314
|
+
this.expect(tt.bracketR);
|
|
315
|
+
|
|
316
|
+
// Finish this MemberExpression node
|
|
317
|
+
base = this.finishNode(node, 'MemberExpression');
|
|
318
|
+
|
|
319
|
+
// Recursively handle any further subscripts (chaining)
|
|
320
|
+
return this.parseSubscripts(
|
|
321
|
+
base,
|
|
322
|
+
startPos,
|
|
323
|
+
startLoc,
|
|
324
|
+
noCalls,
|
|
325
|
+
maybeAsyncArrow,
|
|
326
|
+
optionalChained,
|
|
327
|
+
forInit,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fall back to default parseSubscripts implementation
|
|
333
|
+
return super.parseSubscripts(
|
|
334
|
+
base,
|
|
335
|
+
startPos,
|
|
336
|
+
startLoc,
|
|
337
|
+
noCalls,
|
|
338
|
+
maybeAsyncArrow,
|
|
339
|
+
optionalChained,
|
|
340
|
+
forInit,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
217
343
|
/**
|
|
218
344
|
* Parse expression atom - handles TrackedArray and TrackedObject literals
|
|
219
345
|
* @param {any} [refDestructuringErrors]
|
|
@@ -1230,7 +1356,7 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1230
1356
|
|
|
1231
1357
|
comments = comments
|
|
1232
1358
|
.filter((comment) => comment.start >= index)
|
|
1233
|
-
.map(({ type, value, start, end }) => ({ type, value, start, end }));
|
|
1359
|
+
.map(({ type, value, start, end, loc }) => ({ type, value, start, end, loc }));
|
|
1234
1360
|
|
|
1235
1361
|
walk(ast, null, {
|
|
1236
1362
|
_(node, { next, path }) {
|
|
@@ -1301,11 +1427,56 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1301
1427
|
export function parse(source) {
|
|
1302
1428
|
/** @type {CommentWithLocation[]} */
|
|
1303
1429
|
const comments = [];
|
|
1304
|
-
|
|
1430
|
+
|
|
1431
|
+
// Preprocess step 1: Add trailing commas to single-parameter generics followed by (
|
|
1432
|
+
// This is a workaround for @sveltejs/acorn-typescript limitations with JSX enabled
|
|
1433
|
+
let preprocessedSource = source;
|
|
1434
|
+
let sourceChanged = false;
|
|
1435
|
+
|
|
1436
|
+
preprocessedSource = source.replace(/(<\s*[A-Z][a-zA-Z0-9_$]*\s*)>\s*\(/g, (_, generic) => {
|
|
1437
|
+
sourceChanged = true;
|
|
1438
|
+
// Add trailing comma to disambiguate from JSX
|
|
1439
|
+
return `${generic},>(`;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// Preprocess step 2: Convert generic method shorthand in object literals to function property syntax
|
|
1443
|
+
// Transform `method<T,>(...): ReturnType { body }` to `method: function<T,>(...): ReturnType { body }`
|
|
1444
|
+
// Note: This only applies to object literal methods, not class methods
|
|
1445
|
+
// The trailing comma was already added by step 1
|
|
1446
|
+
preprocessedSource = preprocessedSource.replace(
|
|
1447
|
+
/(\w+)(<[A-Z][a-zA-Z0-9_$,\s]*>)\s*\(([^)]*)\)(\s*:\s*[^{]+)?(\s*\{)/g,
|
|
1448
|
+
(match, methodName, generics, params, returnType, brace, offset) => {
|
|
1449
|
+
// Look backward to determine context
|
|
1450
|
+
let checkPos = offset - 1;
|
|
1451
|
+
while (checkPos >= 0 && /\s/.test(preprocessedSource[checkPos])) checkPos--;
|
|
1452
|
+
const prevChar = preprocessedSource[checkPos];
|
|
1453
|
+
|
|
1454
|
+
// Check if we're inside a class
|
|
1455
|
+
const before = preprocessedSource.substring(Math.max(0, offset - 500), offset);
|
|
1456
|
+
const classMatch = before.match(/\bclass\s+\w+[^{]*\{[^}]*$/);
|
|
1457
|
+
|
|
1458
|
+
// Only transform if we're in an object literal context AND not inside a class
|
|
1459
|
+
if ((prevChar === '{' || prevChar === ',') && !classMatch) {
|
|
1460
|
+
sourceChanged = true;
|
|
1461
|
+
// This is object literal method shorthand - convert to function property
|
|
1462
|
+
// Add trailing comma if not already present
|
|
1463
|
+
const fixedGenerics = generics.includes(',') ? generics : generics.replace('>', ',>');
|
|
1464
|
+
return `${methodName}: function${fixedGenerics}(${params})${returnType || ''}${brace}`;
|
|
1465
|
+
}
|
|
1466
|
+
return match;
|
|
1467
|
+
},
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
// Only mark as preprocessed if we actually changed something
|
|
1471
|
+
if (!sourceChanged) {
|
|
1472
|
+
preprocessedSource = source;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const { onComment, add_comments } = get_comment_handlers(preprocessedSource, comments);
|
|
1305
1476
|
let ast;
|
|
1306
1477
|
|
|
1307
1478
|
try {
|
|
1308
|
-
ast = parser.parse(
|
|
1479
|
+
ast = parser.parse(preprocessedSource, {
|
|
1309
1480
|
sourceType: 'module',
|
|
1310
1481
|
ecmaVersion: 13,
|
|
1311
1482
|
locations: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import {Expression, FunctionExpression} from 'estree' */
|
|
1
|
+
/** @import {Expression, FunctionExpression, Pattern} from 'estree' */
|
|
2
2
|
|
|
3
3
|
import { walk } from 'zimmerframe';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -293,7 +293,6 @@ const visitors = {
|
|
|
293
293
|
|
|
294
294
|
NewExpression(node, context) {
|
|
295
295
|
const callee = node.callee;
|
|
296
|
-
const parent = context.path.at(-1);
|
|
297
296
|
|
|
298
297
|
if (context.state.metadata?.tracking === false) {
|
|
299
298
|
context.state.metadata.tracking = true;
|
|
@@ -305,18 +304,22 @@ const visitors = {
|
|
|
305
304
|
is_inside_call_expression(context) ||
|
|
306
305
|
is_value_static(node)
|
|
307
306
|
) {
|
|
307
|
+
if (!context.state.to_ts) {
|
|
308
|
+
delete node.typeArguments;
|
|
309
|
+
}
|
|
308
310
|
return context.next();
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
313
|
+
const new_node = {
|
|
314
|
+
...node,
|
|
315
|
+
callee: context.visit(callee),
|
|
316
|
+
arguments: node.arguments.map((arg) => context.visit(arg)),
|
|
317
|
+
};
|
|
318
|
+
if (!context.state.to_ts) {
|
|
319
|
+
delete new_node.typeArguments;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return b.call('_$_.with_scope', b.id('__block'), b.thunk(new_node));
|
|
320
323
|
},
|
|
321
324
|
|
|
322
325
|
TrackedArrayExpression(node, context) {
|
|
@@ -366,7 +369,7 @@ const visitors = {
|
|
|
366
369
|
context.state.metadata.tracking = true;
|
|
367
370
|
}
|
|
368
371
|
|
|
369
|
-
if (node.property.type === 'Identifier' && node.property.tracked) {
|
|
372
|
+
if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
|
|
370
373
|
add_ripple_internal_import(context);
|
|
371
374
|
|
|
372
375
|
return b.call(
|
|
@@ -918,8 +921,7 @@ const visitors = {
|
|
|
918
921
|
|
|
919
922
|
if (
|
|
920
923
|
left.type === 'MemberExpression' &&
|
|
921
|
-
left.property.type === 'Identifier' &&
|
|
922
|
-
left.property.tracked
|
|
924
|
+
(left.tracked || (left.property.type === 'Identifier' && left.property.tracked))
|
|
923
925
|
) {
|
|
924
926
|
add_ripple_internal_import(context);
|
|
925
927
|
const operator = node.operator;
|
|
@@ -976,8 +978,7 @@ const visitors = {
|
|
|
976
978
|
|
|
977
979
|
if (
|
|
978
980
|
argument.type === 'MemberExpression' &&
|
|
979
|
-
argument.property.type === 'Identifier' &&
|
|
980
|
-
argument.property.tracked
|
|
981
|
+
(argument.tracked || (argument.property.type === 'Identifier' && argument.property.tracked))
|
|
981
982
|
) {
|
|
982
983
|
add_ripple_internal_import(context);
|
|
983
984
|
context.state.metadata.tracking = true;
|
|
@@ -1052,7 +1053,9 @@ const visitors = {
|
|
|
1052
1053
|
),
|
|
1053
1054
|
),
|
|
1054
1055
|
b.literal(flags),
|
|
1055
|
-
key != null
|
|
1056
|
+
key != null
|
|
1057
|
+
? b.arrow(index ? [pattern, index] : [pattern], context.visit(key))
|
|
1058
|
+
: undefined,
|
|
1056
1059
|
),
|
|
1057
1060
|
),
|
|
1058
1061
|
);
|
|
@@ -1401,6 +1404,9 @@ function transform_ts_child(node, context) {
|
|
|
1401
1404
|
...context,
|
|
1402
1405
|
state: { ...context.state, scope: body_scope },
|
|
1403
1406
|
});
|
|
1407
|
+
if (node.key) {
|
|
1408
|
+
block_body.unshift(b.stmt(visit(node.key)));
|
|
1409
|
+
}
|
|
1404
1410
|
if (node.index) {
|
|
1405
1411
|
block_body.unshift(b.let(visit(node.index), b.literal(0)));
|
|
1406
1412
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`computed tracked properties > should update a property using assignment 1`] = `
|
|
4
|
+
<div>
|
|
5
|
+
<div>
|
|
6
|
+
0
|
|
7
|
+
</div>
|
|
8
|
+
<button>
|
|
9
|
+
Increment
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
exports[`computed tracked properties > should update a property using assignment 2`] = `
|
|
16
|
+
<div>
|
|
17
|
+
<div>
|
|
18
|
+
1
|
|
19
|
+
</div>
|
|
20
|
+
<button>
|
|
21
|
+
Increment
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`computed tracked properties > should update a property using update expressions 1`] = `
|
|
28
|
+
<div>
|
|
29
|
+
<div>
|
|
30
|
+
0
|
|
31
|
+
</div>
|
|
32
|
+
<button>
|
|
33
|
+
Increment
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
exports[`computed tracked properties > should update a property using update expressions 2`] = `
|
|
40
|
+
<div>
|
|
41
|
+
<div>
|
|
42
|
+
1
|
|
43
|
+
</div>
|
|
44
|
+
<button>
|
|
45
|
+
Increment
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
@@ -342,25 +342,33 @@ describe('composite components', () => {
|
|
|
342
342
|
// Ambiguous generics vs JSX / less-than parsing scenarios
|
|
343
343
|
|
|
344
344
|
// // 7. Generic following optional chaining
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
const maybe = {
|
|
346
|
+
factory<T>() {
|
|
347
|
+
return {
|
|
348
|
+
make<U>() {
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const g = maybe?.factory<number>()?.make<boolean>();
|
|
347
355
|
|
|
348
356
|
// // 8. Comparison operator (ensure '<' here NOT misparsed as generics)
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
let x = 10, y = 20;
|
|
358
|
+
const h = x < y ? 'lt' : 'ge';
|
|
351
359
|
|
|
352
360
|
// // 9. Chained comparisons with intervening generics
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
class Box<T> {
|
|
362
|
+
value: T;
|
|
363
|
+
constructor(value?: T) {
|
|
364
|
+
this.value = value;
|
|
365
|
+
}
|
|
366
|
+
open<U>() {
|
|
367
|
+
return new Box<U>();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const limit = 100;
|
|
371
|
+
const i = new Box<number>().value < limit ? 'ok' : 'no';
|
|
364
372
|
|
|
365
373
|
// 10. JSX / Element should still work
|
|
366
374
|
<div class="still-works">
|
|
@@ -401,16 +409,16 @@ describe('composite components', () => {
|
|
|
401
409
|
const m: Extractor = (v) => v.id;
|
|
402
410
|
|
|
403
411
|
// // 15. Generic in angle after "new" + trailing call
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
class Wrapper<T> {
|
|
413
|
+
value: T;
|
|
414
|
+
constructor() {
|
|
415
|
+
this.value = null as unknown as T;
|
|
416
|
+
}
|
|
417
|
+
unwrap<U>() {
|
|
418
|
+
return null as unknown as U;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const n = new Wrapper<number>().unwrap<string>();
|
|
414
422
|
|
|
415
423
|
// // 16. Angle brackets inside type assertion vs generic call
|
|
416
424
|
// function getUnknown(): unknown {
|
|
@@ -427,40 +435,40 @@ describe('composite components', () => {
|
|
|
427
435
|
// const o = (raw as Map<string, number>).get('a');
|
|
428
436
|
|
|
429
437
|
// // 17. Generic with comma + trailing less-than comparison on next token
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
class Pair<T1, T2> {
|
|
439
|
+
first: T1;
|
|
440
|
+
second: T2;
|
|
441
|
+
constructor() {
|
|
442
|
+
this.first = null as unknown as T1;
|
|
443
|
+
this.second = null as unknown as T2;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const p = new Pair<number, string>();
|
|
447
|
+
const q = 1 < 2 ? p : null;
|
|
440
448
|
|
|
441
449
|
// // 18. Nested generics with line breaks resembling JSX indentation
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
450
|
+
interface Node<T> {
|
|
451
|
+
value: T;
|
|
452
|
+
}
|
|
453
|
+
interface Edge<W> {
|
|
454
|
+
weight: W;
|
|
455
|
+
}
|
|
456
|
+
class Graph<N, E> {
|
|
457
|
+
nodes: N[];
|
|
458
|
+
edges: E[];
|
|
459
|
+
constructor() {
|
|
460
|
+
this.nodes = [];
|
|
461
|
+
this.edges = [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const r = new Graph<
|
|
465
|
+
Node<string>,
|
|
466
|
+
Edge<number>
|
|
467
|
+
>();
|
|
460
468
|
|
|
461
469
|
// // 19. Ternary containing generics in both branches
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
let flag = true;
|
|
471
|
+
const s = flag ? new Box<number>() : new Box<string>();
|
|
464
472
|
|
|
465
473
|
// 20. Generic inside template expression
|
|
466
474
|
const t = `length=${new TrackedArray<number>().length}`;
|
|
@@ -473,7 +481,7 @@ describe('composite components', () => {
|
|
|
473
481
|
// function make<T>() {
|
|
474
482
|
// return (value: T) => value;
|
|
475
483
|
// }
|
|
476
|
-
// const v = make<number>()(
|
|
484
|
+
// const v = make<number>()(10);
|
|
477
485
|
|
|
478
486
|
// // 23. Generic followed by tagged template (ensure not confused with JSX)
|
|
479
487
|
// function tagFn<T>(strings: TemplateStringsArray, ...values: T[]) {
|
|
@@ -493,7 +501,7 @@ describe('composite components', () => {
|
|
|
493
501
|
// Additional component focusing on edge crankers
|
|
494
502
|
|
|
495
503
|
// // 28. Generic after parenthesized new expression
|
|
496
|
-
|
|
504
|
+
const aa = (new Box<number>()).open<string>();
|
|
497
505
|
|
|
498
506
|
// // 29. Generic chain right after closing paren of IIFE
|
|
499
507
|
// class Builder<Kind> {
|
|
@@ -518,28 +526,23 @@ describe('composite components', () => {
|
|
|
518
526
|
|
|
519
527
|
|
|
520
528
|
// // 32. Generic with comments inside angle list
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
529
|
+
class Mapper<Key, Value> {
|
|
530
|
+
map: Map<Key, Value>;
|
|
531
|
+
constructor() {
|
|
532
|
+
this.map = new Map<Key, Value>();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const gg = new Mapper<
|
|
536
|
+
// key type
|
|
537
|
+
string,
|
|
538
|
+
/* value type */
|
|
539
|
+
number
|
|
540
|
+
>();
|
|
533
541
|
|
|
534
542
|
// // 33. Map of generic instance as key
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// // 34. Inline assertion then generic call
|
|
538
|
-
|
|
539
|
-
// const asserted = (getUnknown() as Factory).create<number>();
|
|
543
|
+
const mm = new Map<TrackedArray<number>, TrackedArray<string>>();
|
|
540
544
|
}
|
|
541
545
|
|
|
542
|
-
|
|
543
546
|
render(App);
|
|
544
547
|
});
|
|
545
548
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track, TrackedArray, TrackedMap, effect } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('computed tracked properties', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
function render(component) {
|
|
8
|
+
mount(component, {
|
|
9
|
+
target: container
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
document.body.removeChild(container);
|
|
20
|
+
container = null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should update a property using assignment', () => {
|
|
24
|
+
component App() {
|
|
25
|
+
let obj = {
|
|
26
|
+
[0]: track(0),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
<div>{obj.@[0]}</div>
|
|
30
|
+
|
|
31
|
+
<button onClick={() => { obj.@[0] += 1 }}>{"Increment"}</button>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render(App);
|
|
35
|
+
expect(container).toMatchSnapshot();
|
|
36
|
+
|
|
37
|
+
const button = container.querySelector('button');
|
|
38
|
+
button.click();
|
|
39
|
+
flushSync();
|
|
40
|
+
|
|
41
|
+
expect(container).toMatchSnapshot();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should update a property using update expressions', () => {
|
|
45
|
+
component App() {
|
|
46
|
+
let obj = {
|
|
47
|
+
[0]: track(0),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
<div>{obj.@[0]}</div>
|
|
51
|
+
|
|
52
|
+
<button onClick={() => { obj.@[0]++ }}>{"Increment"}</button>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(App);
|
|
56
|
+
expect(container).toMatchSnapshot();
|
|
57
|
+
|
|
58
|
+
const button = container.querySelector('button');
|
|
59
|
+
button.click();
|
|
60
|
+
flushSync();
|
|
61
|
+
|
|
62
|
+
expect(container).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -65,38 +65,149 @@ describe('generic patterns', () => {
|
|
|
65
65
|
render(App);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
it ('tests simple generic function with return type', () => {
|
|
69
|
+
component App() {
|
|
70
|
+
function getBuilder() {
|
|
71
|
+
return {
|
|
72
|
+
build: function<T>() { return 'test' }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render(App);
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it ('tests simple generic function with return type and no arrow function', () => {
|
|
81
|
+
component App() {
|
|
82
|
+
function getBuilder() {
|
|
83
|
+
return {
|
|
84
|
+
build<T>() { return 'test' }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
<div class="still-works">
|
|
89
|
+
<span>{'Test'}</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
const f = getBuilder().build<string>();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
render(App);
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it ('tests simple generic arrow function with return type', () => {
|
|
99
|
+
component App() {
|
|
100
|
+
function getBuilder() {
|
|
101
|
+
return <T>() => ({
|
|
102
|
+
build<U>() { return 'test' }
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
<div class="still-works">
|
|
107
|
+
<span>{'Test'}</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
type ResultType = string;
|
|
111
|
+
const f = getBuilder()<ResultType>().build<string>();
|
|
112
|
+
<div>{f}</div>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(App);
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it ('tests simple generic arrow function with explicit return type', () => {
|
|
119
|
+
component App() {
|
|
120
|
+
function getBuilder() {
|
|
121
|
+
return <T>() => ({
|
|
122
|
+
build<U>(): U { return 'test' }
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
<div class="still-works">
|
|
127
|
+
<span>{'Test'}</span>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
type ResultType = string;
|
|
131
|
+
const f = getBuilder()<ResultType>().build<string>();
|
|
132
|
+
<div>{f}</div>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(App);
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it ('tests complex generic arrow function with return type', () => {
|
|
139
|
+
component App() {
|
|
140
|
+
function getBuilder() {
|
|
141
|
+
return <T>() => ({
|
|
142
|
+
build<U>(): { build: <V>() => V; data: T; key: U } {
|
|
143
|
+
return 'test';
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
<div class="still-works">
|
|
149
|
+
<span>{'Test'}</span>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
type ResultType = string;
|
|
153
|
+
const f = getBuilder()<ResultType>().build<string>();
|
|
154
|
+
<div>{f}</div>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render(App);
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it ('tests more complex generic arrow function with return type', () => {
|
|
161
|
+
component App() {
|
|
162
|
+
function getBuilder() {
|
|
163
|
+
return <T>() => ({
|
|
164
|
+
build<U>(): { build: <V>() => V; data: T; key: U } {
|
|
165
|
+
return {
|
|
166
|
+
build<V>(): V {
|
|
167
|
+
return 'test' as V;
|
|
87
168
|
}
|
|
88
|
-
}
|
|
169
|
+
};
|
|
89
170
|
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
90
173
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
174
|
+
<div class="still-works">
|
|
175
|
+
<span>{'Test'}</span>
|
|
176
|
+
</div>
|
|
94
177
|
|
|
95
|
-
|
|
178
|
+
type ResultType = string;
|
|
179
|
+
const f = getBuilder()<ResultType>().build<string>();
|
|
180
|
+
<div>{f}</div>
|
|
181
|
+
}
|
|
96
182
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
183
|
+
render(App);
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it ('tests most complex generic arrow function with return type', () => {
|
|
187
|
+
component App() {
|
|
188
|
+
function getBuilder() {
|
|
189
|
+
return <T>() => ({
|
|
190
|
+
build<U>(): { build: <V>() => V; data: T; key: U } {
|
|
191
|
+
return {
|
|
192
|
+
build<V>(): V {
|
|
193
|
+
return 42 as V;
|
|
194
|
+
},
|
|
195
|
+
data: undefined as T,
|
|
196
|
+
key: undefined as U
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
<div class="still-works">
|
|
203
|
+
<span>{'Test'}</span>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
type ResultType = string;
|
|
207
|
+
const f = getBuilder()<ResultType>().build<string>();
|
|
208
|
+
<div>{f}</div>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
render(App);
|
|
212
|
+
})
|
|
102
213
|
});
|