ripple 0.3.7 → 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 (119) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +48 -349
  4. package/src/compiler/phases/2-analyze/index.js +343 -52
  5. package/src/compiler/phases/3-transform/client/index.js +28 -160
  6. package/src/compiler/phases/3-transform/segments.js +0 -7
  7. package/src/compiler/phases/3-transform/server/index.js +31 -154
  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 -17
  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 +3 -2
  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/array/array.copy-within.test.ripple +12 -12
  24. package/tests/client/array/array.derived.test.ripple +46 -46
  25. package/tests/client/array/array.iteration.test.ripple +10 -10
  26. package/tests/client/array/array.mutations.test.ripple +20 -20
  27. package/tests/client/array/array.to-methods.test.ripple +6 -6
  28. package/tests/client/async-suspend.test.ripple +5 -5
  29. package/tests/client/basic/basic.attributes.test.ripple +81 -81
  30. package/tests/client/basic/basic.collections.test.ripple +9 -9
  31. package/tests/client/basic/basic.components.test.ripple +28 -28
  32. package/tests/client/basic/basic.errors.test.ripple +46 -18
  33. package/tests/client/basic/basic.events.test.ripple +37 -37
  34. package/tests/client/basic/basic.get-set.test.ripple +6 -6
  35. package/tests/client/basic/basic.reactivity.test.ripple +58 -203
  36. package/tests/client/basic/basic.rendering.test.ripple +19 -19
  37. package/tests/client/basic/basic.utilities.test.ripple +3 -3
  38. package/tests/client/boundaries.test.ripple +12 -12
  39. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +5 -5
  40. package/tests/client/compiler/compiler.assignments.test.ripple +19 -19
  41. package/tests/client/compiler/compiler.basic.test.ripple +46 -27
  42. package/tests/client/compiler/compiler.tracked-access.test.ripple +2 -2
  43. package/tests/client/composite/composite.dynamic-components.test.ripple +9 -9
  44. package/tests/client/composite/composite.props.test.ripple +14 -16
  45. package/tests/client/composite/composite.reactivity.test.ripple +69 -70
  46. package/tests/client/composite/composite.render.test.ripple +3 -3
  47. package/tests/client/computed-properties.test.ripple +4 -4
  48. package/tests/client/date.test.ripple +42 -42
  49. package/tests/client/dynamic-elements.test.ripple +44 -45
  50. package/tests/client/events.test.ripple +70 -70
  51. package/tests/client/for.test.ripple +25 -25
  52. package/tests/client/head.test.ripple +19 -19
  53. package/tests/client/html.test.ripple +3 -3
  54. package/tests/client/input-value.test.ripple +84 -84
  55. package/tests/client/lazy-destructuring.test.ripple +138 -26
  56. package/tests/client/map.test.ripple +16 -16
  57. package/tests/client/media-query.test.ripple +7 -7
  58. package/tests/client/portal.test.ripple +11 -11
  59. package/tests/client/ref.test.ripple +4 -4
  60. package/tests/client/return.test.ripple +52 -52
  61. package/tests/client/set.test.ripple +6 -6
  62. package/tests/client/svg.test.ripple +5 -5
  63. package/tests/client/switch.test.ripple +44 -44
  64. package/tests/client/try.test.ripple +5 -5
  65. package/tests/client/url/url.derived.test.ripple +6 -6
  66. package/tests/client/url-search-params/url-search-params.derived.test.ripple +8 -8
  67. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +10 -10
  68. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +10 -10
  69. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +18 -18
  70. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +2 -2
  71. package/tests/hydration/compiled/client/events.js +25 -25
  72. package/tests/hydration/compiled/client/for.js +70 -66
  73. package/tests/hydration/compiled/client/head.js +25 -25
  74. package/tests/hydration/compiled/client/hmr.js +2 -2
  75. package/tests/hydration/compiled/client/html.js +3 -3
  76. package/tests/hydration/compiled/client/if-children.js +24 -24
  77. package/tests/hydration/compiled/client/if.js +18 -18
  78. package/tests/hydration/compiled/client/mixed-control-flow.js +9 -9
  79. package/tests/hydration/compiled/client/portal.js +3 -3
  80. package/tests/hydration/compiled/client/reactivity.js +16 -16
  81. package/tests/hydration/compiled/client/return.js +40 -40
  82. package/tests/hydration/compiled/client/switch.js +12 -12
  83. package/tests/hydration/compiled/server/events.js +19 -19
  84. package/tests/hydration/compiled/server/for.js +41 -41
  85. package/tests/hydration/compiled/server/head.js +26 -26
  86. package/tests/hydration/compiled/server/hmr.js +2 -2
  87. package/tests/hydration/compiled/server/html.js +2 -2
  88. package/tests/hydration/compiled/server/if-children.js +16 -16
  89. package/tests/hydration/compiled/server/if.js +11 -11
  90. package/tests/hydration/compiled/server/mixed-control-flow.js +6 -6
  91. package/tests/hydration/compiled/server/portal.js +2 -2
  92. package/tests/hydration/compiled/server/reactivity.js +16 -16
  93. package/tests/hydration/compiled/server/return.js +25 -25
  94. package/tests/hydration/compiled/server/switch.js +8 -8
  95. package/tests/hydration/components/events.ripple +25 -25
  96. package/tests/hydration/components/for.ripple +66 -66
  97. package/tests/hydration/components/head.ripple +16 -16
  98. package/tests/hydration/components/hmr.ripple +2 -2
  99. package/tests/hydration/components/html.ripple +3 -3
  100. package/tests/hydration/components/if-children.ripple +24 -24
  101. package/tests/hydration/components/if.ripple +18 -18
  102. package/tests/hydration/components/mixed-control-flow.ripple +9 -9
  103. package/tests/hydration/components/portal.ripple +3 -3
  104. package/tests/hydration/components/reactivity.ripple +16 -16
  105. package/tests/hydration/components/return.ripple +40 -40
  106. package/tests/hydration/components/switch.ripple +20 -20
  107. package/tests/server/await.test.ripple +3 -3
  108. package/tests/server/basic.attributes.test.ripple +34 -34
  109. package/tests/server/basic.components.test.ripple +10 -10
  110. package/tests/server/basic.test.ripple +38 -40
  111. package/tests/server/compiler.test.ripple +22 -0
  112. package/tests/server/composite.props.test.ripple +12 -14
  113. package/tests/server/dynamic-elements.test.ripple +15 -15
  114. package/tests/server/head.test.ripple +11 -11
  115. package/tests/server/lazy-destructuring.test.ripple +92 -13
  116. package/tsconfig.typecheck.json +4 -0
  117. package/types/index.d.ts +0 -19
  118. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  119. package/tests/client/tracked-expression.test.ripple +0 -26
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.9
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.3.9
9
+
10
+ ## 0.3.8
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies []:
15
+ - ripple@0.3.8
16
+
3
17
  ## 0.3.7
4
18
 
5
19
  ### 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.7",
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.7"
108
+ "ripple": "0.3.9"
109
109
  }
110
110
  }
@@ -52,16 +52,6 @@ function DestructuringErrors() {
52
52
  return this;
53
53
  }
54
54
 
55
- /**
56
- * @param {AST.Identifier | ESTreeJSX.JSXIdentifier} node
57
- * @param {string} name
58
- */
59
- function set_tracked_name(node, name) {
60
- node.name = name.slice(1);
61
- node.metadata ??= { path: [] };
62
- node.metadata.source_name = name;
63
- }
64
-
65
55
  /**
66
56
  * Convert JSX node types to regular JavaScript node types
67
57
  * @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
@@ -423,7 +413,9 @@ function RipplePlugin(config) {
423
413
  /** @type {AST.Property} */ (prop).method = true;
424
414
  /** @type {AST.Property} */ (prop).kind = 'init';
425
415
  /** @type {AST.Property} */ (prop).value = this.parseMethod(false, false);
426
- /** @type {AST.Property} */ (prop).value.typeParameters = typeParameters;
416
+ /** @type {AST.FunctionExpression} */ (
417
+ /** @type {AST.Property} */ (prop).value
418
+ ).typeParameters = typeParameters;
427
419
  return;
428
420
  }
429
421
  }
@@ -676,202 +668,9 @@ function RipplePlugin(config) {
676
668
  }
677
669
  }
678
670
  }
679
- if (code === 64) {
680
- // @ character
681
- // Look ahead to see if this is followed by a valid identifier character or opening paren
682
- if (this.pos + 1 < this.input.length) {
683
- const nextChar = this.input.charCodeAt(this.pos + 1);
684
-
685
- // Check if this is @( for unboxing expression syntax
686
- if (nextChar === 40) {
687
- // ( character
688
- this.pos += 2; // skip '@('
689
- return this.finishToken(tt.parenL, '@(');
690
- }
691
-
692
- // Check if the next character can start an identifier
693
- if (
694
- (nextChar >= 65 && nextChar <= 90) || // A-Z
695
- (nextChar >= 97 && nextChar <= 122) || // a-z
696
- nextChar === 95 ||
697
- nextChar === 36
698
- ) {
699
- // _ or $
700
-
701
- // Check if we're in an expression context
702
- // In JSX expressions, inside parentheses, assignments, etc.
703
- // we want to treat @ as an identifier prefix rather than decorator
704
- const currentType = this.type;
705
- /**
706
- * @param {Parse.TokenType} type
707
- * @param {Parse.Parser} parser
708
- * @param {Parse.TokTypes} tt
709
- * @returns {boolean}
710
- */
711
- function inExpression(type, parser, tt) {
712
- return (
713
- parser.exprAllowed ||
714
- type === tt.braceL || // Inside { }
715
- type === tt.parenL || // Inside ( )
716
- type === tt.eq || // After =
717
- type === tt.comma || // After ,
718
- type === tt.colon || // After :
719
- type === tt.question || // After ?
720
- type === tt.logicalOR || // After ||
721
- type === tt.logicalAND || // After &&
722
- type === tt.dot || // After . (for member expressions like obj.@prop)
723
- type === tt.questionDot // After ?. (for optional chaining like obj?.@prop)
724
- );
725
- }
726
-
727
- /**
728
- * @param {Parse.Parser} parser
729
- * @param {Parse.TokTypes} tt
730
- * @returns {boolean}
731
- */
732
- function inAwait(parser, tt) {
733
- return currentType === tt.name &&
734
- parser.value === 'await' &&
735
- parser.canAwait &&
736
- parser.preToken
737
- ? inExpression(parser.preToken, parser, tt)
738
- : false;
739
- }
740
-
741
- if (inExpression(currentType, this, tt) || inAwait(this, tt)) {
742
- return this.readAtIdentifier();
743
- }
744
- }
745
- }
746
- }
747
671
  return super.getTokenFromCode(code);
748
672
  }
749
673
 
750
- /**
751
- * Read an @ prefixed identifier
752
- * @type {Parse.Parser['readAtIdentifier']}
753
- */
754
- readAtIdentifier() {
755
- const start = this.pos;
756
- this.pos++; // skip '@'
757
-
758
- // Read the identifier part manually
759
- let word = '';
760
- while (this.pos < this.input.length) {
761
- const ch = this.input.charCodeAt(this.pos);
762
- if (
763
- (ch >= 65 && ch <= 90) || // A-Z
764
- (ch >= 97 && ch <= 122) || // a-z
765
- (ch >= 48 && ch <= 57) || // 0-9
766
- ch === 95 ||
767
- ch === 36
768
- ) {
769
- // _ or $
770
- word += this.input[this.pos++];
771
- } else {
772
- break;
773
- }
774
- }
775
-
776
- if (word === '') {
777
- this.raise(start, 'Invalid @ identifier');
778
- }
779
-
780
- // Return the full identifier including @
781
- return this.finishToken(tt.name, '@' + word);
782
- }
783
-
784
- /**
785
- * Override parseIdent to mark @ identifiers as tracked
786
- * @type {Parse.Parser['parseIdent']}
787
- */
788
- parseIdent(liberal) {
789
- const node = /** @type {AST.Identifier &AST.NodeWithLocation} */ (
790
- super.parseIdent(liberal)
791
- );
792
- if (node.name && node.name.startsWith('@')) {
793
- set_tracked_name(node, node.name);
794
- node.tracked = true;
795
- }
796
- return node;
797
- }
798
-
799
- /**
800
- * Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
801
- * @type {Parse.Parser['parseSubscripts']}
802
- */
803
- parseSubscripts(
804
- base,
805
- startPos,
806
- startLoc,
807
- noCalls,
808
- maybeAsyncArrow,
809
- optionalChained,
810
- forInit,
811
- ) {
812
- // Check for `.@[` pattern for reactive computed member access
813
- const isDotOrOptional = this.type === tt.dot || this.type === tt.questionDot;
814
-
815
- if (isDotOrOptional) {
816
- // Check the next two characters without consuming tokens
817
- // this.pos currently points AFTER the dot token
818
- const nextChar = this.input.charCodeAt(this.pos);
819
- const charAfter = this.input.charCodeAt(this.pos + 1);
820
-
821
- // Check for @[ pattern (@ = 64, [ = 91)
822
- if (nextChar === 64 && charAfter === 91) {
823
- const node = /** @type {AST.MemberExpression} */ (this.startNodeAt(startPos, startLoc));
824
- node.object = base;
825
- node.computed = true;
826
- node.optional = this.type === tt.questionDot;
827
- node.tracked = true;
828
-
829
- // Consume the dot/questionDot token
830
- this.next();
831
-
832
- // Manually skip the @ character
833
- this.pos += 1;
834
-
835
- // Now call finishToken to properly consume the [ bracket
836
- this.finishToken(tt.bracketL);
837
-
838
- // Now we're positioned correctly to parse the expression
839
- this.next(); // Move to first token inside brackets
840
-
841
- // Parse the expression inside brackets
842
- node.property = this.parseExpression();
843
-
844
- // Expect closing bracket
845
- this.expect(tt.bracketR);
846
-
847
- // Finish this MemberExpression node
848
- base = /** @type {AST.MemberExpression} */ (this.finishNode(node, 'MemberExpression'));
849
-
850
- // Recursively handle any further subscripts (chaining)
851
- return this.parseSubscripts(
852
- base,
853
- startPos,
854
- startLoc,
855
- noCalls,
856
- maybeAsyncArrow,
857
- optionalChained,
858
- forInit,
859
- );
860
- }
861
- }
862
-
863
- // Fall back to default parseSubscripts implementation
864
- return super.parseSubscripts(
865
- base,
866
- startPos,
867
- startLoc,
868
- noCalls,
869
- maybeAsyncArrow,
870
- optionalChained,
871
- forInit,
872
- );
873
- }
874
-
875
674
  /**
876
675
  * Override isLet to recognize `let &{` and `let &[` as variable declarations.
877
676
  * Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
@@ -908,7 +707,7 @@ function RipplePlugin(config) {
908
707
  // & directly followed by { or [ — lazy destructuring
909
708
  this.next(); // consume &, now current token is { or [
910
709
  const pattern = super.parseBindingAtom();
911
- pattern.lazy = true;
710
+ /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (pattern).lazy = true;
912
711
  return pattern;
913
712
  }
914
713
  }
@@ -923,11 +722,6 @@ function RipplePlugin(config) {
923
722
  const lookahead_type = this.lookahead().type;
924
723
  const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
925
724
 
926
- // Check if this is @(expression) for unboxing tracked values
927
- if (this.type === tt.parenL && this.value === '@(') {
928
- return this.parseTrackedExpression();
929
- }
930
-
931
725
  // Check if this is #server identifier for server function calls
932
726
  if (this.type === tt.name && this.value === '#server') {
933
727
  const node = this.startNode();
@@ -968,31 +762,6 @@ function RipplePlugin(config) {
968
762
  return expr;
969
763
  }
970
764
 
971
- /**
972
- * Parse `@(expression)` syntax for unboxing tracked values
973
- * Creates a TrackedExpression node with the argument property
974
- * @type {Parse.Parser['parseTrackedExpression']}
975
- */
976
- parseTrackedExpression() {
977
- const node = /** @type {AST.TrackedExpression} */ (this.startNode());
978
- this.next(); // consume '@(' token
979
- node.argument = this.parseExpression();
980
- this.expect(tt.parenR); // expect ')'
981
- return this.finishNode(node, 'TrackedExpression');
982
- }
983
-
984
- /**
985
- * Override to allow TrackedExpression as a valid lvalue for update expressions
986
- * @type {Parse.Parser['checkLValSimple']}
987
- */
988
- checkLValSimple(expr, bindingType, checkClashes) {
989
- // Allow TrackedExpression as a valid lvalue for ++/-- operators
990
- if (expr.type === 'TrackedExpression') {
991
- return;
992
- }
993
- return super.checkLValSimple(expr, bindingType, checkClashes);
994
- }
995
-
996
765
  /**
997
766
  * Override checkLocalExport to check all scopes in the scope stack.
998
767
  * This is needed because server blocks create nested scopes, but exports
@@ -1131,6 +900,7 @@ function RipplePlugin(config) {
1131
900
  return this.parseFor(node, null);
1132
901
  }
1133
902
 
903
+ // @ts-ignore — acorn internal: isLet accepts 0 args at runtime
1134
904
  let isLet = this.isLet();
1135
905
  if (this.type === tt._var || this.type === tt._const || isLet) {
1136
906
  let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
@@ -1173,7 +943,9 @@ function RipplePlugin(config) {
1173
943
  }
1174
944
 
1175
945
  let containsEsc = this.containsEsc;
1176
- let refDestructuringErrors = new DestructuringErrors();
946
+ let refDestructuringErrors = new /** @type {new () => Parse.DestructuringErrors} */ (
947
+ /** @type {unknown} */ (DestructuringErrors)
948
+ )();
1177
949
  let initPos = this.start;
1178
950
  let init_expr =
1179
951
  awaitAt > -1
@@ -1342,7 +1114,6 @@ function RipplePlugin(config) {
1342
1114
  jsx_parseExpressionContainer() {
1343
1115
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1344
1116
  this.next();
1345
- let tracked = false;
1346
1117
 
1347
1118
  if (this.value === 'html') {
1348
1119
  node.html = true;
@@ -1353,20 +1124,12 @@ function RipplePlugin(config) {
1353
1124
  '"html" is a Ripple keyword and must be used in the form {html some_content}',
1354
1125
  );
1355
1126
  }
1356
- if (this.type.label === '@') {
1357
- this.next(); // consume @
1358
- tracked = true;
1359
- }
1360
1127
  }
1361
1128
 
1362
1129
  node.expression =
1363
1130
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1364
1131
  this.expect(tt.braceR);
1365
1132
 
1366
- if (tracked && node.expression.type === 'Identifier') {
1367
- node.expression.tracked = true;
1368
- }
1369
-
1370
1133
  return this.finishNode(node, 'JSXExpressionContainer');
1371
1134
  }
1372
1135
 
@@ -1429,10 +1192,6 @@ function RipplePlugin(config) {
1429
1192
  } else {
1430
1193
  const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1431
1194
  id.tracked = false;
1432
- if (id.name.startsWith('@')) {
1433
- set_tracked_name(id, id.name);
1434
- id.tracked = true;
1435
- }
1436
1195
  this.finishNode(id, 'Identifier');
1437
1196
  /** @type {AST.Attribute} */ (node).name = id;
1438
1197
  /** @type {AST.Attribute} */ (node).value = id;
@@ -1484,14 +1243,6 @@ function RipplePlugin(config) {
1484
1243
  // Unexpected token after @
1485
1244
  this.unexpected();
1486
1245
  }
1487
- } else if (
1488
- (this.type === tt.name || this.type === tstt.jsxName) &&
1489
- this.value &&
1490
- /** @type {string} */ (this.value).startsWith('@')
1491
- ) {
1492
- set_tracked_name(node, /** @type {string} */ (this.value));
1493
- node.tracked = true;
1494
- this.next();
1495
1246
  } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1496
1247
  node.name = /** @type {string} */ (this.value);
1497
1248
  node.tracked = false; // Explicitly mark as not tracked
@@ -1525,40 +1276,9 @@ function RipplePlugin(config) {
1525
1276
  )
1526
1277
  );
1527
1278
  memberExpr.object = node;
1528
-
1529
- // Check for .@[expression] syntax for tracked computed member access
1530
- // After eating the dot, check if the current token is @ followed by [
1531
- if (this.type.label === '@') {
1532
- // Check if the next character after @ is [
1533
- const nextChar = this.input.charCodeAt(this.pos);
1534
-
1535
- if (nextChar === 91) {
1536
- // [ character
1537
- memberExpr.computed = true;
1538
-
1539
- // Consume the @ token
1540
- this.next();
1541
-
1542
- // Now this.type should be bracketL
1543
- // Consume the [ and parse the expression inside
1544
- this.expect(tt.bracketL);
1545
-
1546
- // Parse the expression inside brackets
1547
- memberExpr.property = /** @type {ESTreeJSX.JSXIdentifier} */ (this.parseExpression());
1548
- /** @type {AST.TrackedNode} */ (memberExpr.property).tracked = true;
1549
-
1550
- // Expect closing bracket
1551
- this.expect(tt.bracketR);
1552
- } else {
1553
- // @ not followed by [, treat as regular tracked identifier
1554
- memberExpr.property = this.jsx_parseIdentifier();
1555
- memberExpr.computed = false;
1556
- }
1557
- } else {
1558
- // Regular dot notation
1559
- memberExpr.property = this.jsx_parseIdentifier();
1560
- memberExpr.computed = false;
1561
- }
1279
+ memberExpr.property = this.jsx_parseIdentifier();
1280
+ memberExpr.computed = false;
1281
+ memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
1562
1282
  while (this.eat(tt.dot)) {
1563
1283
  let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1564
1284
  this.startNodeAt(
@@ -1571,7 +1291,7 @@ function RipplePlugin(config) {
1571
1291
  newMemberExpr.computed = false;
1572
1292
  memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
1573
1293
  }
1574
- return this.finishNode(memberExpr, 'JSXMemberExpression');
1294
+ return memberExpr;
1575
1295
  }
1576
1296
  return node;
1577
1297
  }
@@ -1842,8 +1562,9 @@ function RipplePlugin(config) {
1842
1562
  if (expression.type === 'Literal') {
1843
1563
  expression.was_expression = true;
1844
1564
  }
1845
- /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (attr.value) =
1846
- expression;
1565
+ // @ts-ignore intentional AST node conversion from JSX to Ripple
1566
+ /** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
1567
+ /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
1847
1568
  }
1848
1569
  }
1849
1570
  }
@@ -2355,53 +2076,6 @@ function RipplePlugin(config) {
2355
2076
  this.awaitPos = 0;
2356
2077
  return this.parseComponent({ requireName: true, declareName: true });
2357
2078
  }
2358
- if (this.type.label === '@') {
2359
- // Try to parse as an expression statement first using tryParse
2360
- // This allows us to handle Ripple @ syntax like @count++ without
2361
- // interfering with legitimate decorator syntax
2362
- this.skip_decorator = true;
2363
- const expressionResult = this.tryParse(() => {
2364
- const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
2365
- this.next();
2366
- // Force expression context to ensure @ is tokenized correctly
2367
- const old_expr_allowed = this.exprAllowed;
2368
- this.exprAllowed = true;
2369
- node.expression = this.parseExpression();
2370
-
2371
- if (node.expression.type === 'UpdateExpression') {
2372
- /** @type {AST.Expression} */
2373
- let object = node.expression.argument;
2374
- while (object.type === 'MemberExpression') {
2375
- object = /** @type {AST.Expression} */ (object.object);
2376
- }
2377
- if (object.type === 'Identifier') {
2378
- object.tracked = true;
2379
- }
2380
- } else if (node.expression.type === 'AssignmentExpression') {
2381
- /** @type {AST.Expression | AST.Pattern | AST.Identifier} */
2382
- let object = node.expression.left;
2383
- while (object.type === 'MemberExpression') {
2384
- object = /** @type {AST.Expression} */ (object.object);
2385
- }
2386
- if (object.type === 'Identifier') {
2387
- object.tracked = true;
2388
- }
2389
- } else if (node.expression.type === 'Identifier') {
2390
- node.expression.tracked = true;
2391
- } else {
2392
- // TODO?
2393
- }
2394
-
2395
- this.exprAllowed = old_expr_allowed;
2396
- return this.finishNode(node, 'ExpressionStatement');
2397
- });
2398
- this.skip_decorator = false;
2399
-
2400
- // If parsing as expression statement succeeded, use that result
2401
- if (expressionResult.node) {
2402
- return expressionResult.node;
2403
- }
2404
- }
2405
2079
 
2406
2080
  if (this.type === tstt.jsxTagStart) {
2407
2081
  this.next();
@@ -2416,6 +2090,37 @@ function RipplePlugin(config) {
2416
2090
  return node;
2417
2091
  }
2418
2092
 
2093
+ // &[ or &{ at statement level — lazy destructuring assignment
2094
+ // e.g., &[data] = track(0); or &{x, y} = obj;
2095
+ if (this.type === tt.bitwiseAND) {
2096
+ const charAfterAmp = this.input.charCodeAt(this.end);
2097
+ if (charAfterAmp === 123 || charAfterAmp === 91) {
2098
+ const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
2099
+ const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
2100
+ this.next(); // consume &
2101
+ // Parse the left-hand side (array or object expression)
2102
+ const left = /** @type {AST.ArrayPattern | AST.ObjectPattern} */ (
2103
+ /** @type {unknown} */ (this.parseExprAtom())
2104
+ );
2105
+ // Convert expression to destructuring pattern
2106
+ this.toAssignable(left, false);
2107
+ left.lazy = true;
2108
+ // Expect = operator
2109
+ this.expect(tt.eq);
2110
+ // Parse the right-hand side
2111
+ assign_node.operator = '=';
2112
+ assign_node.left = left;
2113
+ assign_node.right = /** @type {AST.Expression} */ (this.parseMaybeAssign());
2114
+ node.expression = /** @type {AST.AssignmentExpression} */ (
2115
+ this.finishNode(assign_node, 'AssignmentExpression')
2116
+ );
2117
+ this.semicolon();
2118
+ return /** @type {AST.ExpressionStatement} */ (
2119
+ this.finishNode(node, 'ExpressionStatement')
2120
+ );
2121
+ }
2122
+ }
2123
+
2419
2124
  return super.parseStatement(context, topLevel, exports);
2420
2125
  }
2421
2126
 
@@ -2809,15 +2514,9 @@ function get_comment_handlers(source, comments, index = 0) {
2809
2514
  isSwitchCaseSibling = true;
2810
2515
  } else if (parent.type === 'SwitchCase') {
2811
2516
  node_array = parent.consequent;
2812
- } else if (
2813
- parent.type === 'ArrayExpression' ||
2814
- parent.type === 'RippleArrayExpression'
2815
- ) {
2517
+ } else if (parent.type === 'ArrayExpression') {
2816
2518
  node_array = parent.elements;
2817
- } else if (
2818
- parent.type === 'ObjectExpression' ||
2819
- parent.type === 'RippleObjectExpression'
2820
- ) {
2519
+ } else if (parent.type === 'ObjectExpression') {
2821
2520
  node_array = parent.properties;
2822
2521
  } else if (
2823
2522
  parent.type === 'FunctionDeclaration' ||