ripple 0.3.8 → 0.3.9
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/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/src/compiler/phases/1-parse/index.js +13 -157
- package/src/compiler/phases/2-analyze/index.js +289 -43
- package/src/compiler/phases/3-transform/client/index.js +9 -157
- package/src/compiler/phases/3-transform/segments.js +0 -7
- package/src/compiler/phases/3-transform/server/index.js +15 -130
- package/src/compiler/types/acorn.d.ts +1 -1
- package/src/compiler/types/estree.d.ts +1 -1
- package/src/compiler/types/import.d.ts +0 -2
- package/src/compiler/types/index.d.ts +5 -17
- package/src/compiler/types/parse.d.ts +1 -9
- package/src/compiler/utils.js +53 -20
- package/src/runtime/index-client.js +2 -13
- package/src/runtime/index-server.js +2 -2
- package/src/runtime/internal/client/bindings.js +3 -1
- package/src/runtime/internal/client/composite.js +1 -0
- package/src/runtime/internal/client/events.js +1 -1
- package/src/runtime/internal/client/head.js +3 -4
- package/src/runtime/internal/client/index.js +0 -1
- package/src/runtime/internal/client/runtime.js +0 -52
- package/src/runtime/internal/server/index.js +31 -55
- package/tests/client/basic/basic.errors.test.ripple +28 -0
- package/tests/client/basic/basic.reactivity.test.ripple +10 -155
- package/tests/client/compiler/compiler.basic.test.ripple +31 -12
- package/tests/client/composite/composite.props.test.ripple +5 -7
- package/tests/client/composite/composite.reactivity.test.ripple +35 -36
- package/tests/client/dynamic-elements.test.ripple +3 -4
- package/tests/client/lazy-destructuring.test.ripple +69 -12
- package/tests/server/compiler.test.ripple +22 -0
- package/tests/server/composite.props.test.ripple +5 -7
- package/tests/server/dynamic-elements.test.ripple +3 -4
- package/tests/server/lazy-destructuring.test.ripple +68 -12
- package/tsconfig.typecheck.json +4 -0
- package/types/index.d.ts +0 -19
- package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
- package/tests/client/tracked-expression.test.ripple +0 -26
package/CHANGELOG.md
CHANGED
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.3.
|
|
6
|
+
"version": "0.3.9",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -105,6 +105,6 @@
|
|
|
105
105
|
"vscode-languageserver-types": "^3.17.5"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
|
-
"ripple": "0.3.
|
|
108
|
+
"ripple": "0.3.9"
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -413,7 +413,9 @@ function RipplePlugin(config) {
|
|
|
413
413
|
/** @type {AST.Property} */ (prop).method = true;
|
|
414
414
|
/** @type {AST.Property} */ (prop).kind = 'init';
|
|
415
415
|
/** @type {AST.Property} */ (prop).value = this.parseMethod(false, false);
|
|
416
|
-
/** @type {AST.
|
|
416
|
+
/** @type {AST.FunctionExpression} */ (
|
|
417
|
+
/** @type {AST.Property} */ (prop).value
|
|
418
|
+
).typeParameters = typeParameters;
|
|
417
419
|
return;
|
|
418
420
|
}
|
|
419
421
|
}
|
|
@@ -666,99 +668,9 @@ function RipplePlugin(config) {
|
|
|
666
668
|
}
|
|
667
669
|
}
|
|
668
670
|
}
|
|
669
|
-
if (code === 64) {
|
|
670
|
-
// @ character
|
|
671
|
-
// Look ahead to see if this is followed by an opening paren
|
|
672
|
-
if (this.pos + 1 < this.input.length) {
|
|
673
|
-
const nextChar = this.input.charCodeAt(this.pos + 1);
|
|
674
|
-
|
|
675
|
-
// Check if this is @( for unboxing expression syntax
|
|
676
|
-
if (nextChar === 40) {
|
|
677
|
-
// ( character
|
|
678
|
-
this.pos += 2; // skip '@('
|
|
679
|
-
return this.finishToken(tt.parenL, '@(');
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
671
|
return super.getTokenFromCode(code);
|
|
684
672
|
}
|
|
685
673
|
|
|
686
|
-
/**
|
|
687
|
-
* Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
|
|
688
|
-
* @type {Parse.Parser['parseSubscripts']}
|
|
689
|
-
*/
|
|
690
|
-
parseSubscripts(
|
|
691
|
-
base,
|
|
692
|
-
startPos,
|
|
693
|
-
startLoc,
|
|
694
|
-
noCalls,
|
|
695
|
-
maybeAsyncArrow,
|
|
696
|
-
optionalChained,
|
|
697
|
-
forInit,
|
|
698
|
-
) {
|
|
699
|
-
// Check for `.@[` pattern for reactive computed member access
|
|
700
|
-
const isDotOrOptional = this.type === tt.dot || this.type === tt.questionDot;
|
|
701
|
-
|
|
702
|
-
if (isDotOrOptional) {
|
|
703
|
-
// Check the next two characters without consuming tokens
|
|
704
|
-
// this.pos currently points AFTER the dot token
|
|
705
|
-
const nextChar = this.input.charCodeAt(this.pos);
|
|
706
|
-
const charAfter = this.input.charCodeAt(this.pos + 1);
|
|
707
|
-
|
|
708
|
-
// Check for @[ pattern (@ = 64, [ = 91)
|
|
709
|
-
if (nextChar === 64 && charAfter === 91) {
|
|
710
|
-
const node = /** @type {AST.MemberExpression} */ (this.startNodeAt(startPos, startLoc));
|
|
711
|
-
node.object = base;
|
|
712
|
-
node.computed = true;
|
|
713
|
-
node.optional = this.type === tt.questionDot;
|
|
714
|
-
node.tracked = true;
|
|
715
|
-
|
|
716
|
-
// Consume the dot/questionDot token
|
|
717
|
-
this.next();
|
|
718
|
-
|
|
719
|
-
// Manually skip the @ character
|
|
720
|
-
this.pos += 1;
|
|
721
|
-
|
|
722
|
-
// Now call finishToken to properly consume the [ bracket
|
|
723
|
-
this.finishToken(tt.bracketL);
|
|
724
|
-
|
|
725
|
-
// Now we're positioned correctly to parse the expression
|
|
726
|
-
this.next(); // Move to first token inside brackets
|
|
727
|
-
|
|
728
|
-
// Parse the expression inside brackets
|
|
729
|
-
node.property = this.parseExpression();
|
|
730
|
-
|
|
731
|
-
// Expect closing bracket
|
|
732
|
-
this.expect(tt.bracketR);
|
|
733
|
-
|
|
734
|
-
// Finish this MemberExpression node
|
|
735
|
-
base = /** @type {AST.MemberExpression} */ (this.finishNode(node, 'MemberExpression'));
|
|
736
|
-
|
|
737
|
-
// Recursively handle any further subscripts (chaining)
|
|
738
|
-
return this.parseSubscripts(
|
|
739
|
-
base,
|
|
740
|
-
startPos,
|
|
741
|
-
startLoc,
|
|
742
|
-
noCalls,
|
|
743
|
-
maybeAsyncArrow,
|
|
744
|
-
optionalChained,
|
|
745
|
-
forInit,
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Fall back to default parseSubscripts implementation
|
|
751
|
-
return super.parseSubscripts(
|
|
752
|
-
base,
|
|
753
|
-
startPos,
|
|
754
|
-
startLoc,
|
|
755
|
-
noCalls,
|
|
756
|
-
maybeAsyncArrow,
|
|
757
|
-
optionalChained,
|
|
758
|
-
forInit,
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
674
|
/**
|
|
763
675
|
* Override isLet to recognize `let &{` and `let &[` as variable declarations.
|
|
764
676
|
* Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
|
|
@@ -795,7 +707,7 @@ function RipplePlugin(config) {
|
|
|
795
707
|
// & directly followed by { or [ — lazy destructuring
|
|
796
708
|
this.next(); // consume &, now current token is { or [
|
|
797
709
|
const pattern = super.parseBindingAtom();
|
|
798
|
-
pattern.lazy = true;
|
|
710
|
+
/** @type {AST.ObjectPattern | AST.ArrayPattern} */ (pattern).lazy = true;
|
|
799
711
|
return pattern;
|
|
800
712
|
}
|
|
801
713
|
}
|
|
@@ -810,11 +722,6 @@ function RipplePlugin(config) {
|
|
|
810
722
|
const lookahead_type = this.lookahead().type;
|
|
811
723
|
const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
|
|
812
724
|
|
|
813
|
-
// Check if this is @(expression) for unboxing tracked values
|
|
814
|
-
if (this.type === tt.parenL && this.value === '@(') {
|
|
815
|
-
return this.parseTrackedExpression();
|
|
816
|
-
}
|
|
817
|
-
|
|
818
725
|
// Check if this is #server identifier for server function calls
|
|
819
726
|
if (this.type === tt.name && this.value === '#server') {
|
|
820
727
|
const node = this.startNode();
|
|
@@ -855,31 +762,6 @@ function RipplePlugin(config) {
|
|
|
855
762
|
return expr;
|
|
856
763
|
}
|
|
857
764
|
|
|
858
|
-
/**
|
|
859
|
-
* Parse `@(expression)` syntax for unboxing tracked values
|
|
860
|
-
* Creates a TrackedExpression node with the argument property
|
|
861
|
-
* @type {Parse.Parser['parseTrackedExpression']}
|
|
862
|
-
*/
|
|
863
|
-
parseTrackedExpression() {
|
|
864
|
-
const node = /** @type {AST.TrackedExpression} */ (this.startNode());
|
|
865
|
-
this.next(); // consume '@(' token
|
|
866
|
-
node.argument = this.parseExpression();
|
|
867
|
-
this.expect(tt.parenR); // expect ')'
|
|
868
|
-
return this.finishNode(node, 'TrackedExpression');
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Override to allow TrackedExpression as a valid lvalue for update expressions
|
|
873
|
-
* @type {Parse.Parser['checkLValSimple']}
|
|
874
|
-
*/
|
|
875
|
-
checkLValSimple(expr, bindingType, checkClashes) {
|
|
876
|
-
// Allow TrackedExpression as a valid lvalue for ++/-- operators
|
|
877
|
-
if (expr.type === 'TrackedExpression') {
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
return super.checkLValSimple(expr, bindingType, checkClashes);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
765
|
/**
|
|
884
766
|
* Override checkLocalExport to check all scopes in the scope stack.
|
|
885
767
|
* This is needed because server blocks create nested scopes, but exports
|
|
@@ -1018,6 +900,7 @@ function RipplePlugin(config) {
|
|
|
1018
900
|
return this.parseFor(node, null);
|
|
1019
901
|
}
|
|
1020
902
|
|
|
903
|
+
// @ts-ignore — acorn internal: isLet accepts 0 args at runtime
|
|
1021
904
|
let isLet = this.isLet();
|
|
1022
905
|
if (this.type === tt._var || this.type === tt._const || isLet) {
|
|
1023
906
|
let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
|
|
@@ -1060,7 +943,9 @@ function RipplePlugin(config) {
|
|
|
1060
943
|
}
|
|
1061
944
|
|
|
1062
945
|
let containsEsc = this.containsEsc;
|
|
1063
|
-
let refDestructuringErrors = new DestructuringErrors(
|
|
946
|
+
let refDestructuringErrors = new /** @type {new () => Parse.DestructuringErrors} */ (
|
|
947
|
+
/** @type {unknown} */ (DestructuringErrors)
|
|
948
|
+
)();
|
|
1064
949
|
let initPos = this.start;
|
|
1065
950
|
let init_expr =
|
|
1066
951
|
awaitAt > -1
|
|
@@ -1391,38 +1276,8 @@ function RipplePlugin(config) {
|
|
|
1391
1276
|
)
|
|
1392
1277
|
);
|
|
1393
1278
|
memberExpr.object = node;
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
// After eating the dot, check if the current token is @ followed by [
|
|
1397
|
-
if (this.type.label === '@') {
|
|
1398
|
-
// Check if the next character after @ is [
|
|
1399
|
-
const nextChar = this.input.charCodeAt(this.pos);
|
|
1400
|
-
|
|
1401
|
-
if (nextChar === 91) {
|
|
1402
|
-
// [ character
|
|
1403
|
-
memberExpr.computed = true;
|
|
1404
|
-
|
|
1405
|
-
// Consume the @ token
|
|
1406
|
-
this.next();
|
|
1407
|
-
|
|
1408
|
-
// Now this.type should be bracketL
|
|
1409
|
-
// Consume the [ and parse the expression inside
|
|
1410
|
-
this.expect(tt.bracketL);
|
|
1411
|
-
|
|
1412
|
-
// Parse the expression inside brackets
|
|
1413
|
-
memberExpr.property = /** @type {ESTreeJSX.JSXIdentifier} */ (this.parseExpression());
|
|
1414
|
-
/** @type {AST.TrackedNode} */ (memberExpr.property).tracked = true;
|
|
1415
|
-
|
|
1416
|
-
// Expect closing bracket
|
|
1417
|
-
this.expect(tt.bracketR);
|
|
1418
|
-
} else {
|
|
1419
|
-
this.unexpected();
|
|
1420
|
-
}
|
|
1421
|
-
} else {
|
|
1422
|
-
// Regular dot notation
|
|
1423
|
-
memberExpr.property = this.jsx_parseIdentifier();
|
|
1424
|
-
memberExpr.computed = false;
|
|
1425
|
-
}
|
|
1279
|
+
memberExpr.property = this.jsx_parseIdentifier();
|
|
1280
|
+
memberExpr.computed = false;
|
|
1426
1281
|
memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
|
|
1427
1282
|
while (this.eat(tt.dot)) {
|
|
1428
1283
|
let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
|
|
@@ -1707,8 +1562,9 @@ function RipplePlugin(config) {
|
|
|
1707
1562
|
if (expression.type === 'Literal') {
|
|
1708
1563
|
expression.was_expression = true;
|
|
1709
1564
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1565
|
+
// @ts-ignore — intentional AST node conversion from JSX to Ripple
|
|
1566
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
|
|
1567
|
+
/** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
|
|
1712
1568
|
}
|
|
1713
1569
|
}
|
|
1714
1570
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
AnalysisResult,
|
|
5
5
|
AnalysisState,
|
|
6
6
|
AnalysisContext,
|
|
7
|
+
Context,
|
|
7
8
|
ScopeInterface,
|
|
8
9
|
Visitors,
|
|
9
10
|
TopScopedClasses,
|
|
@@ -329,14 +330,185 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
|
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
/**
|
|
332
|
-
*
|
|
333
|
+
* @param {AST.Pattern} pattern
|
|
334
|
+
* @returns {AST.TypeNode | undefined}
|
|
335
|
+
*/
|
|
336
|
+
function get_pattern_type_annotation(pattern) {
|
|
337
|
+
return pattern.typeAnnotation?.typeAnnotation;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {AST.TypeNode | undefined} type_annotation
|
|
342
|
+
* @returns {AST.TypeNode | undefined}
|
|
343
|
+
*/
|
|
344
|
+
function unwrap_type_annotation(type_annotation) {
|
|
345
|
+
/** @type {AST.TypeNode | undefined} */
|
|
346
|
+
let annotation = type_annotation;
|
|
347
|
+
|
|
348
|
+
while (annotation) {
|
|
349
|
+
if (annotation.type === 'TSParenthesizedType') {
|
|
350
|
+
annotation = annotation.typeAnnotation;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (annotation.type === 'TSOptionalType') {
|
|
354
|
+
annotation = annotation.typeAnnotation;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return annotation;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @param {AST.TypeNode} type_annotation
|
|
365
|
+
* @returns {AST.TypeNode}
|
|
366
|
+
*/
|
|
367
|
+
function normalize_tuple_element_type(type_annotation) {
|
|
368
|
+
/** @type {AST.TypeNode} */
|
|
369
|
+
let annotation = type_annotation;
|
|
370
|
+
|
|
371
|
+
while (true) {
|
|
372
|
+
if (annotation.type === 'TSNamedTupleMember') {
|
|
373
|
+
annotation = annotation.elementType;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (annotation.type === 'TSParenthesizedType') {
|
|
377
|
+
annotation = annotation.typeAnnotation;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (annotation.type === 'TSOptionalType') {
|
|
381
|
+
annotation = annotation.typeAnnotation;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return annotation;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {AST.Expression} key
|
|
392
|
+
* @returns {string | null}
|
|
393
|
+
*/
|
|
394
|
+
function get_object_pattern_key_name(key) {
|
|
395
|
+
if (key.type === 'Identifier') {
|
|
396
|
+
return key.name;
|
|
397
|
+
}
|
|
398
|
+
if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
|
|
399
|
+
return String(key.value);
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param {AST.PropertyNameNonComputed} key
|
|
406
|
+
* @returns {string | null}
|
|
407
|
+
*/
|
|
408
|
+
function get_type_property_key_name(key) {
|
|
409
|
+
if (key.type === 'Identifier') {
|
|
410
|
+
return key.name;
|
|
411
|
+
}
|
|
412
|
+
if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
|
|
413
|
+
return String(key.value);
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @param {AST.TypeNode | undefined} type_annotation
|
|
420
|
+
* @param {AST.Property | AST.RestElement} property
|
|
421
|
+
* @returns {AST.TypeNode | undefined}
|
|
422
|
+
*/
|
|
423
|
+
function get_object_property_type_annotation(type_annotation, property) {
|
|
424
|
+
if (property.type === 'RestElement' || property.computed) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const object_type_annotation = unwrap_type_annotation(type_annotation);
|
|
429
|
+
if (object_type_annotation?.type !== 'TSTypeLiteral') {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const key_name = get_object_pattern_key_name(property.key);
|
|
434
|
+
if (key_name === null) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const member of object_type_annotation.members) {
|
|
439
|
+
if (member.type !== 'TSPropertySignature' || member.computed) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const member_key_name = get_type_property_key_name(member.key);
|
|
443
|
+
if (member_key_name === key_name) {
|
|
444
|
+
return member.typeAnnotation?.typeAnnotation;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @param {AST.TypeNode | undefined} type_annotation
|
|
453
|
+
* @param {number} index
|
|
454
|
+
* @param {boolean} is_rest
|
|
455
|
+
* @returns {AST.TypeNode | undefined}
|
|
456
|
+
*/
|
|
457
|
+
function get_array_element_type_annotation(type_annotation, index, is_rest) {
|
|
458
|
+
const array_type_annotation = unwrap_type_annotation(type_annotation);
|
|
459
|
+
|
|
460
|
+
if (array_type_annotation?.type === 'TSArrayType') {
|
|
461
|
+
return array_type_annotation.elementType;
|
|
462
|
+
}
|
|
463
|
+
if (array_type_annotation?.type !== 'TSTupleType') {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (is_rest) {
|
|
468
|
+
for (let i = array_type_annotation.elementTypes.length - 1; i >= 0; i -= 1) {
|
|
469
|
+
const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[i]);
|
|
470
|
+
if (element_type.type === 'TSRestType') {
|
|
471
|
+
return element_type.typeAnnotation;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (index < array_type_annotation.elementTypes.length) {
|
|
478
|
+
const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[index]);
|
|
479
|
+
if (element_type.type === 'TSRestType') {
|
|
480
|
+
const rest_type_annotation = unwrap_type_annotation(element_type.typeAnnotation);
|
|
481
|
+
return rest_type_annotation?.type === 'TSArrayType'
|
|
482
|
+
? rest_type_annotation.elementType
|
|
483
|
+
: element_type.typeAnnotation;
|
|
484
|
+
}
|
|
485
|
+
return element_type;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const last_element = array_type_annotation.elementTypes.at(-1);
|
|
489
|
+
if (!last_element) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const normalized_last_element = normalize_tuple_element_type(last_element);
|
|
493
|
+
if (normalized_last_element.type === 'TSRestType') {
|
|
494
|
+
const rest_type_annotation = unwrap_type_annotation(normalized_last_element.typeAnnotation);
|
|
495
|
+
return rest_type_annotation?.type === 'TSArrayType'
|
|
496
|
+
? rest_type_annotation.elementType
|
|
497
|
+
: normalized_last_element.typeAnnotation;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Checks if a parameter source has a Tracked<T> type annotation imported from ripple.
|
|
333
505
|
* This is used to determine if lazy array destructuring should use the track tuple fast path.
|
|
334
|
-
* @param {AST.
|
|
506
|
+
* @param {AST.TypeNode | undefined} type_annotation - The source type annotation
|
|
335
507
|
* @param {AnalysisContext} context - The analysis context
|
|
336
508
|
* @returns {boolean}
|
|
337
509
|
*/
|
|
338
|
-
function is_param_tracked_type(
|
|
339
|
-
const annotation =
|
|
510
|
+
function is_param_tracked_type(type_annotation, context) {
|
|
511
|
+
const annotation = unwrap_type_annotation(type_annotation);
|
|
340
512
|
|
|
341
513
|
if (
|
|
342
514
|
annotation?.type === 'TSTypeReference' &&
|
|
@@ -357,6 +529,75 @@ function is_param_tracked_type(param, context) {
|
|
|
357
529
|
return false;
|
|
358
530
|
}
|
|
359
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Sets up lazy transforms for any lazy subpatterns nested inside a function or component param.
|
|
534
|
+
* @param {AST.Pattern} pattern
|
|
535
|
+
* @param {AnalysisContext} context
|
|
536
|
+
* @param {AST.TypeNode | undefined} [type_annotation]
|
|
537
|
+
*/
|
|
538
|
+
function setup_nested_lazy_param_transforms(pattern, context, type_annotation = undefined) {
|
|
539
|
+
const pattern_type_annotation = get_pattern_type_annotation(pattern) ?? type_annotation;
|
|
540
|
+
|
|
541
|
+
switch (pattern.type) {
|
|
542
|
+
case 'AssignmentPattern':
|
|
543
|
+
setup_nested_lazy_param_transforms(pattern.left, context, pattern_type_annotation);
|
|
544
|
+
return;
|
|
545
|
+
|
|
546
|
+
case 'RestElement':
|
|
547
|
+
setup_nested_lazy_param_transforms(pattern.argument, context, pattern_type_annotation);
|
|
548
|
+
return;
|
|
549
|
+
|
|
550
|
+
case 'ObjectPattern':
|
|
551
|
+
case 'ArrayPattern': {
|
|
552
|
+
if (pattern.lazy) {
|
|
553
|
+
const param_id = b.id(context.state.scope.generate('lazy'));
|
|
554
|
+
const is_tracked_type =
|
|
555
|
+
pattern.type === 'ArrayPattern' &&
|
|
556
|
+
is_param_tracked_type(pattern_type_annotation, context);
|
|
557
|
+
|
|
558
|
+
setup_lazy_transforms(pattern, param_id, context.state, true, is_tracked_type);
|
|
559
|
+
pattern.metadata = { ...pattern.metadata, lazy_id: param_id.name };
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (pattern.type === 'ObjectPattern') {
|
|
564
|
+
for (const property of pattern.properties) {
|
|
565
|
+
const property_type_annotation = get_object_property_type_annotation(
|
|
566
|
+
pattern_type_annotation,
|
|
567
|
+
property,
|
|
568
|
+
);
|
|
569
|
+
if (property.type === 'RestElement') {
|
|
570
|
+
setup_nested_lazy_param_transforms(
|
|
571
|
+
property.argument,
|
|
572
|
+
context,
|
|
573
|
+
property_type_annotation,
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
setup_nested_lazy_param_transforms(property.value, context, property_type_annotation);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
for (let i = 0; i < pattern.elements.length; i += 1) {
|
|
581
|
+
const element = pattern.elements[i];
|
|
582
|
+
if (element !== null) {
|
|
583
|
+
setup_nested_lazy_param_transforms(
|
|
584
|
+
element,
|
|
585
|
+
context,
|
|
586
|
+
get_array_element_type_annotation(
|
|
587
|
+
pattern_type_annotation,
|
|
588
|
+
i,
|
|
589
|
+
element.type === 'RestElement',
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
360
601
|
/**
|
|
361
602
|
* @param {AST.Function} node
|
|
362
603
|
* @param {AnalysisContext} context
|
|
@@ -371,16 +612,11 @@ function visit_function(node, context) {
|
|
|
371
612
|
for (let i = 0; i < node.params.length; i++) {
|
|
372
613
|
const param_node = node.params[i];
|
|
373
614
|
const param = param_node.type === 'AssignmentPattern' ? param_node.left : param_node;
|
|
615
|
+
const param_type_annotation =
|
|
616
|
+
get_pattern_type_annotation(param) ?? param_node.typeAnnotation?.typeAnnotation;
|
|
374
617
|
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
// For ArrayPattern params with a Tracked<T> type annotation from ripple,
|
|
378
|
-
// use the track tuple fast path (get/set instead of source[0]/source[1])
|
|
379
|
-
const is_tracked_type =
|
|
380
|
-
param.type === 'ArrayPattern' && is_param_tracked_type(param, context);
|
|
381
|
-
setup_lazy_transforms(param, param_id, context.state, true, is_tracked_type);
|
|
382
|
-
// Store the generated identifier name on the pattern for the transform phase
|
|
383
|
-
param.metadata = { ...param.metadata, lazy_id: param_id.name };
|
|
618
|
+
if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
|
|
619
|
+
setup_nested_lazy_param_transforms(param, context, param_type_annotation);
|
|
384
620
|
}
|
|
385
621
|
}
|
|
386
622
|
|
|
@@ -484,19 +720,14 @@ function unwrap_template_expression(expression) {
|
|
|
484
720
|
|
|
485
721
|
/**
|
|
486
722
|
* @param {AST.Expression} expression
|
|
487
|
-
* @param {AnalysisState}
|
|
723
|
+
* @param {Context<AST.Node, AnalysisState>} context
|
|
488
724
|
* @returns {boolean}
|
|
489
725
|
*/
|
|
490
|
-
function is_children_template_expression(expression,
|
|
726
|
+
function is_children_template_expression(expression, context) {
|
|
727
|
+
const component = context.path.findLast((node) => node.type === 'Component');
|
|
728
|
+
const component_scope = component ? context.state.scopes.get(component) : null;
|
|
491
729
|
const unwrapped = unwrap_template_expression(expression);
|
|
492
730
|
|
|
493
|
-
if (unwrapped.type === 'TrackedExpression') {
|
|
494
|
-
return is_children_template_expression(
|
|
495
|
-
/** @type {AST.Expression} */ (unwrapped.argument),
|
|
496
|
-
state,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
731
|
if (unwrapped.type === 'MemberExpression') {
|
|
501
732
|
let property_name = null;
|
|
502
733
|
|
|
@@ -514,13 +745,25 @@ function is_children_template_expression(expression, state) {
|
|
|
514
745
|
const target = unwrap_template_expression(/** @type {AST.Expression} */ (unwrapped.object));
|
|
515
746
|
|
|
516
747
|
if (target.type === 'Identifier') {
|
|
517
|
-
const binding = state.scope.get(target.name);
|
|
518
|
-
return binding?.declaration_kind === 'param';
|
|
748
|
+
const binding = context.state.scope.get(target.name);
|
|
749
|
+
return binding?.declaration_kind === 'param' && binding.scope === component_scope;
|
|
519
750
|
}
|
|
520
751
|
}
|
|
521
752
|
}
|
|
522
753
|
|
|
523
|
-
|
|
754
|
+
if (unwrapped.type !== 'Identifier' || unwrapped.name !== 'children') {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const binding = context.state.scope.get(unwrapped.name);
|
|
759
|
+
return (
|
|
760
|
+
(binding?.declaration_kind === 'param' ||
|
|
761
|
+
binding?.kind === 'prop' ||
|
|
762
|
+
binding?.kind === 'prop_fallback' ||
|
|
763
|
+
binding?.kind === 'lazy' ||
|
|
764
|
+
binding?.kind === 'lazy_fallback') &&
|
|
765
|
+
binding.scope === component_scope
|
|
766
|
+
);
|
|
524
767
|
}
|
|
525
768
|
|
|
526
769
|
/** @type {Visitors<AST.Node, AnalysisState>} */
|
|
@@ -632,16 +875,6 @@ const visitors = {
|
|
|
632
875
|
MemberExpression(node, context) {
|
|
633
876
|
const parent = context.path.at(-1);
|
|
634
877
|
|
|
635
|
-
if (
|
|
636
|
-
context.state.metadata?.tracking === false &&
|
|
637
|
-
parent?.type !== 'AssignmentExpression' &&
|
|
638
|
-
(node.tracked ||
|
|
639
|
-
((node.property.type === 'Identifier' || node.property.type === 'Literal') &&
|
|
640
|
-
/** @type {AST.TrackedNode} */ (node.property).tracked))
|
|
641
|
-
) {
|
|
642
|
-
context.state.metadata.tracking = true;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
878
|
// Track #style.className or #style['className'] references
|
|
646
879
|
if (node.object.type === 'StyleIdentifier') {
|
|
647
880
|
const component = is_inside_component(context, true);
|
|
@@ -761,6 +994,19 @@ const visitors = {
|
|
|
761
994
|
|
|
762
995
|
const callee = node.callee;
|
|
763
996
|
|
|
997
|
+
if (
|
|
998
|
+
!context.path.some((path_node) => path_node.type === 'TsxCompat') &&
|
|
999
|
+
is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
|
|
1000
|
+
) {
|
|
1001
|
+
error(
|
|
1002
|
+
'`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
|
|
1003
|
+
context.state.analysis.module.filename,
|
|
1004
|
+
callee,
|
|
1005
|
+
context.state.loose ? context.state.analysis.errors : undefined,
|
|
1006
|
+
context.state.analysis.comments,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
764
1010
|
if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
|
|
765
1011
|
error(
|
|
766
1012
|
'`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
|
|
@@ -913,9 +1159,13 @@ const visitors = {
|
|
|
913
1159
|
if (node.params.length > 0) {
|
|
914
1160
|
const props = node.params[0];
|
|
915
1161
|
|
|
916
|
-
if (
|
|
1162
|
+
if (props.type === 'ObjectPattern' || props.type === 'ArrayPattern') {
|
|
917
1163
|
// Lazy destructuring: &{...} or &[...] — set up lazy transforms
|
|
918
|
-
|
|
1164
|
+
if (props.lazy) {
|
|
1165
|
+
setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
|
|
1166
|
+
} else {
|
|
1167
|
+
setup_nested_lazy_param_transforms(props, context, get_pattern_type_annotation(props));
|
|
1168
|
+
}
|
|
919
1169
|
} else if (props.type === 'AssignmentPattern') {
|
|
920
1170
|
error(
|
|
921
1171
|
'Props are always an object, use destructured props with default values instead',
|
|
@@ -1463,6 +1713,7 @@ const visitors = {
|
|
|
1463
1713
|
|
|
1464
1714
|
const { state, visit, path } = context;
|
|
1465
1715
|
const is_dom_element = is_element_dom_element(node);
|
|
1716
|
+
/** @type {Set<AST.Identifier>} */
|
|
1466
1717
|
const attribute_names = new Set();
|
|
1467
1718
|
|
|
1468
1719
|
mark_control_flow_has_template(path);
|
|
@@ -1718,12 +1969,7 @@ const visitors = {
|
|
|
1718
1969
|
Text(node, context) {
|
|
1719
1970
|
mark_control_flow_has_template(context.path);
|
|
1720
1971
|
|
|
1721
|
-
if (
|
|
1722
|
-
is_children_template_expression(
|
|
1723
|
-
/** @type {AST.Expression} */ (node.expression),
|
|
1724
|
-
context.state,
|
|
1725
|
-
)
|
|
1726
|
-
) {
|
|
1972
|
+
if (is_children_template_expression(/** @type {AST.Expression} */ (node.expression), context)) {
|
|
1727
1973
|
error(
|
|
1728
1974
|
'`children` cannot be rendered using text interpolation. Use `<children />` instead.',
|
|
1729
1975
|
context.state.analysis.module.filename,
|