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.
Files changed (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +13 -157
  4. package/src/compiler/phases/2-analyze/index.js +289 -43
  5. package/src/compiler/phases/3-transform/client/index.js +9 -157
  6. package/src/compiler/phases/3-transform/segments.js +0 -7
  7. package/src/compiler/phases/3-transform/server/index.js +15 -130
  8. package/src/compiler/types/acorn.d.ts +1 -1
  9. package/src/compiler/types/estree.d.ts +1 -1
  10. package/src/compiler/types/import.d.ts +0 -2
  11. package/src/compiler/types/index.d.ts +5 -17
  12. package/src/compiler/types/parse.d.ts +1 -9
  13. package/src/compiler/utils.js +53 -20
  14. package/src/runtime/index-client.js +2 -13
  15. package/src/runtime/index-server.js +2 -2
  16. package/src/runtime/internal/client/bindings.js +3 -1
  17. package/src/runtime/internal/client/composite.js +1 -0
  18. package/src/runtime/internal/client/events.js +1 -1
  19. package/src/runtime/internal/client/head.js +3 -4
  20. package/src/runtime/internal/client/index.js +0 -1
  21. package/src/runtime/internal/client/runtime.js +0 -52
  22. package/src/runtime/internal/server/index.js +31 -55
  23. package/tests/client/basic/basic.errors.test.ripple +28 -0
  24. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  25. package/tests/client/compiler/compiler.basic.test.ripple +31 -12
  26. package/tests/client/composite/composite.props.test.ripple +5 -7
  27. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  28. package/tests/client/dynamic-elements.test.ripple +3 -4
  29. package/tests/client/lazy-destructuring.test.ripple +69 -12
  30. package/tests/server/compiler.test.ripple +22 -0
  31. package/tests/server/composite.props.test.ripple +5 -7
  32. package/tests/server/dynamic-elements.test.ripple +3 -4
  33. package/tests/server/lazy-destructuring.test.ripple +68 -12
  34. package/tsconfig.typecheck.json +4 -0
  35. package/types/index.d.ts +0 -19
  36. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  37. package/tests/client/tracked-expression.test.ripple +0 -26
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.9
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.3.9
9
+
3
10
  ## 0.3.8
4
11
 
5
12
  ### Patch Changes
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.8",
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.8"
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.Property} */ (prop).value.typeParameters = typeParameters;
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
- // Check for .@[expression] syntax for tracked computed member access
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
- /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (attr.value) =
1711
- expression;
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
- * Checks if a function parameter has a Tracked<T> type annotation imported from ripple.
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.ArrayPattern} param - The parameter pattern node
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(param, context) {
339
- const annotation = param.typeAnnotation?.typeAnnotation;
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 ((param.type === 'ObjectPattern' || param.type === 'ArrayPattern') && param.lazy) {
376
- const param_id = b.id(context.state.scope.generate('param'));
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} state
723
+ * @param {Context<AST.Node, AnalysisState>} context
488
724
  * @returns {boolean}
489
725
  */
490
- function is_children_template_expression(expression, state) {
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
- return unwrapped.type === 'Identifier' && unwrapped.name === 'children';
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 ((props.type === 'ObjectPattern' || props.type === 'ArrayPattern') && props.lazy) {
1162
+ if (props.type === 'ObjectPattern' || props.type === 'ArrayPattern') {
917
1163
  // Lazy destructuring: &{...} or &[...] — set up lazy transforms
918
- setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
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,