ripple 0.3.7 → 0.3.8

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 (100) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +37 -194
  4. package/src/compiler/phases/2-analyze/index.js +63 -18
  5. package/src/compiler/phases/3-transform/client/index.js +19 -3
  6. package/src/compiler/phases/3-transform/server/index.js +16 -24
  7. package/src/compiler/types/parse.d.ts +0 -8
  8. package/src/runtime/internal/client/composite.js +2 -2
  9. package/tests/client/array/array.copy-within.test.ripple +12 -12
  10. package/tests/client/array/array.derived.test.ripple +46 -46
  11. package/tests/client/array/array.iteration.test.ripple +10 -10
  12. package/tests/client/array/array.mutations.test.ripple +20 -20
  13. package/tests/client/array/array.to-methods.test.ripple +6 -6
  14. package/tests/client/async-suspend.test.ripple +5 -5
  15. package/tests/client/basic/basic.attributes.test.ripple +81 -81
  16. package/tests/client/basic/basic.collections.test.ripple +9 -9
  17. package/tests/client/basic/basic.components.test.ripple +28 -28
  18. package/tests/client/basic/basic.errors.test.ripple +18 -18
  19. package/tests/client/basic/basic.events.test.ripple +37 -37
  20. package/tests/client/basic/basic.get-set.test.ripple +6 -6
  21. package/tests/client/basic/basic.reactivity.test.ripple +68 -68
  22. package/tests/client/basic/basic.rendering.test.ripple +19 -19
  23. package/tests/client/basic/basic.utilities.test.ripple +3 -3
  24. package/tests/client/boundaries.test.ripple +12 -12
  25. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +5 -5
  26. package/tests/client/compiler/compiler.assignments.test.ripple +19 -19
  27. package/tests/client/compiler/compiler.basic.test.ripple +16 -16
  28. package/tests/client/compiler/compiler.tracked-access.test.ripple +2 -2
  29. package/tests/client/composite/composite.dynamic-components.test.ripple +9 -9
  30. package/tests/client/composite/composite.props.test.ripple +11 -11
  31. package/tests/client/composite/composite.reactivity.test.ripple +43 -43
  32. package/tests/client/composite/composite.render.test.ripple +3 -3
  33. package/tests/client/computed-properties.test.ripple +4 -4
  34. package/tests/client/date.test.ripple +42 -42
  35. package/tests/client/dynamic-elements.test.ripple +42 -42
  36. package/tests/client/events.test.ripple +70 -70
  37. package/tests/client/for.test.ripple +25 -25
  38. package/tests/client/head.test.ripple +19 -19
  39. package/tests/client/html.test.ripple +3 -3
  40. package/tests/client/input-value.test.ripple +84 -84
  41. package/tests/client/lazy-destructuring.test.ripple +71 -16
  42. package/tests/client/map.test.ripple +16 -16
  43. package/tests/client/media-query.test.ripple +7 -7
  44. package/tests/client/portal.test.ripple +11 -11
  45. package/tests/client/ref.test.ripple +4 -4
  46. package/tests/client/return.test.ripple +52 -52
  47. package/tests/client/set.test.ripple +6 -6
  48. package/tests/client/svg.test.ripple +5 -5
  49. package/tests/client/switch.test.ripple +44 -44
  50. package/tests/client/try.test.ripple +5 -5
  51. package/tests/client/url/url.derived.test.ripple +6 -6
  52. package/tests/client/url-search-params/url-search-params.derived.test.ripple +8 -8
  53. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +10 -10
  54. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +10 -10
  55. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +18 -18
  56. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +2 -2
  57. package/tests/hydration/compiled/client/events.js +25 -25
  58. package/tests/hydration/compiled/client/for.js +70 -66
  59. package/tests/hydration/compiled/client/head.js +25 -25
  60. package/tests/hydration/compiled/client/hmr.js +2 -2
  61. package/tests/hydration/compiled/client/html.js +3 -3
  62. package/tests/hydration/compiled/client/if-children.js +24 -24
  63. package/tests/hydration/compiled/client/if.js +18 -18
  64. package/tests/hydration/compiled/client/mixed-control-flow.js +9 -9
  65. package/tests/hydration/compiled/client/portal.js +3 -3
  66. package/tests/hydration/compiled/client/reactivity.js +16 -16
  67. package/tests/hydration/compiled/client/return.js +40 -40
  68. package/tests/hydration/compiled/client/switch.js +12 -12
  69. package/tests/hydration/compiled/server/events.js +19 -19
  70. package/tests/hydration/compiled/server/for.js +41 -41
  71. package/tests/hydration/compiled/server/head.js +26 -26
  72. package/tests/hydration/compiled/server/hmr.js +2 -2
  73. package/tests/hydration/compiled/server/html.js +2 -2
  74. package/tests/hydration/compiled/server/if-children.js +16 -16
  75. package/tests/hydration/compiled/server/if.js +11 -11
  76. package/tests/hydration/compiled/server/mixed-control-flow.js +6 -6
  77. package/tests/hydration/compiled/server/portal.js +2 -2
  78. package/tests/hydration/compiled/server/reactivity.js +16 -16
  79. package/tests/hydration/compiled/server/return.js +25 -25
  80. package/tests/hydration/compiled/server/switch.js +8 -8
  81. package/tests/hydration/components/events.ripple +25 -25
  82. package/tests/hydration/components/for.ripple +66 -66
  83. package/tests/hydration/components/head.ripple +16 -16
  84. package/tests/hydration/components/hmr.ripple +2 -2
  85. package/tests/hydration/components/html.ripple +3 -3
  86. package/tests/hydration/components/if-children.ripple +24 -24
  87. package/tests/hydration/components/if.ripple +18 -18
  88. package/tests/hydration/components/mixed-control-flow.ripple +9 -9
  89. package/tests/hydration/components/portal.ripple +3 -3
  90. package/tests/hydration/components/reactivity.ripple +16 -16
  91. package/tests/hydration/components/return.ripple +40 -40
  92. package/tests/hydration/components/switch.ripple +20 -20
  93. package/tests/server/await.test.ripple +3 -3
  94. package/tests/server/basic.attributes.test.ripple +34 -34
  95. package/tests/server/basic.components.test.ripple +10 -10
  96. package/tests/server/basic.test.ripple +38 -40
  97. package/tests/server/composite.props.test.ripple +9 -9
  98. package/tests/server/dynamic-elements.test.ripple +13 -12
  99. package/tests/server/head.test.ripple +11 -11
  100. package/tests/server/lazy-destructuring.test.ripple +27 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.3.8
9
+
3
10
  ## 0.3.7
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.7",
6
+ "version": "0.3.8",
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.8"
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
@@ -678,7 +668,7 @@ function RipplePlugin(config) {
678
668
  }
679
669
  if (code === 64) {
680
670
  // @ character
681
- // Look ahead to see if this is followed by a valid identifier character or opening paren
671
+ // Look ahead to see if this is followed by an opening paren
682
672
  if (this.pos + 1 < this.input.length) {
683
673
  const nextChar = this.input.charCodeAt(this.pos + 1);
684
674
 
@@ -688,114 +678,11 @@ function RipplePlugin(config) {
688
678
  this.pos += 2; // skip '@('
689
679
  return this.finishToken(tt.parenL, '@(');
690
680
  }
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
681
  }
746
682
  }
747
683
  return super.getTokenFromCode(code);
748
684
  }
749
685
 
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
686
  /**
800
687
  * Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
801
688
  * @type {Parse.Parser['parseSubscripts']}
@@ -1342,7 +1229,6 @@ function RipplePlugin(config) {
1342
1229
  jsx_parseExpressionContainer() {
1343
1230
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1344
1231
  this.next();
1345
- let tracked = false;
1346
1232
 
1347
1233
  if (this.value === 'html') {
1348
1234
  node.html = true;
@@ -1353,20 +1239,12 @@ function RipplePlugin(config) {
1353
1239
  '"html" is a Ripple keyword and must be used in the form {html some_content}',
1354
1240
  );
1355
1241
  }
1356
- if (this.type.label === '@') {
1357
- this.next(); // consume @
1358
- tracked = true;
1359
- }
1360
1242
  }
1361
1243
 
1362
1244
  node.expression =
1363
1245
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1364
1246
  this.expect(tt.braceR);
1365
1247
 
1366
- if (tracked && node.expression.type === 'Identifier') {
1367
- node.expression.tracked = true;
1368
- }
1369
-
1370
1248
  return this.finishNode(node, 'JSXExpressionContainer');
1371
1249
  }
1372
1250
 
@@ -1429,10 +1307,6 @@ function RipplePlugin(config) {
1429
1307
  } else {
1430
1308
  const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1431
1309
  id.tracked = false;
1432
- if (id.name.startsWith('@')) {
1433
- set_tracked_name(id, id.name);
1434
- id.tracked = true;
1435
- }
1436
1310
  this.finishNode(id, 'Identifier');
1437
1311
  /** @type {AST.Attribute} */ (node).name = id;
1438
1312
  /** @type {AST.Attribute} */ (node).value = id;
@@ -1484,14 +1358,6 @@ function RipplePlugin(config) {
1484
1358
  // Unexpected token after @
1485
1359
  this.unexpected();
1486
1360
  }
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
1361
  } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1496
1362
  node.name = /** @type {string} */ (this.value);
1497
1363
  node.tracked = false; // Explicitly mark as not tracked
@@ -1550,15 +1416,14 @@ function RipplePlugin(config) {
1550
1416
  // Expect closing bracket
1551
1417
  this.expect(tt.bracketR);
1552
1418
  } else {
1553
- // @ not followed by [, treat as regular tracked identifier
1554
- memberExpr.property = this.jsx_parseIdentifier();
1555
- memberExpr.computed = false;
1419
+ this.unexpected();
1556
1420
  }
1557
1421
  } else {
1558
1422
  // Regular dot notation
1559
1423
  memberExpr.property = this.jsx_parseIdentifier();
1560
1424
  memberExpr.computed = false;
1561
1425
  }
1426
+ memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
1562
1427
  while (this.eat(tt.dot)) {
1563
1428
  let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1564
1429
  this.startNodeAt(
@@ -1571,7 +1436,7 @@ function RipplePlugin(config) {
1571
1436
  newMemberExpr.computed = false;
1572
1437
  memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
1573
1438
  }
1574
- return this.finishNode(memberExpr, 'JSXMemberExpression');
1439
+ return memberExpr;
1575
1440
  }
1576
1441
  return node;
1577
1442
  }
@@ -2355,53 +2220,6 @@ function RipplePlugin(config) {
2355
2220
  this.awaitPos = 0;
2356
2221
  return this.parseComponent({ requireName: true, declareName: true });
2357
2222
  }
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
2223
 
2406
2224
  if (this.type === tstt.jsxTagStart) {
2407
2225
  this.next();
@@ -2416,6 +2234,37 @@ function RipplePlugin(config) {
2416
2234
  return node;
2417
2235
  }
2418
2236
 
2237
+ // &[ or &{ at statement level — lazy destructuring assignment
2238
+ // e.g., &[data] = track(0); or &{x, y} = obj;
2239
+ if (this.type === tt.bitwiseAND) {
2240
+ const charAfterAmp = this.input.charCodeAt(this.end);
2241
+ if (charAfterAmp === 123 || charAfterAmp === 91) {
2242
+ const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
2243
+ const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
2244
+ this.next(); // consume &
2245
+ // Parse the left-hand side (array or object expression)
2246
+ const left = /** @type {AST.ArrayPattern | AST.ObjectPattern} */ (
2247
+ /** @type {unknown} */ (this.parseExprAtom())
2248
+ );
2249
+ // Convert expression to destructuring pattern
2250
+ this.toAssignable(left, false);
2251
+ left.lazy = true;
2252
+ // Expect = operator
2253
+ this.expect(tt.eq);
2254
+ // Parse the right-hand side
2255
+ assign_node.operator = '=';
2256
+ assign_node.left = left;
2257
+ assign_node.right = /** @type {AST.Expression} */ (this.parseMaybeAssign());
2258
+ node.expression = /** @type {AST.AssignmentExpression} */ (
2259
+ this.finishNode(assign_node, 'AssignmentExpression')
2260
+ );
2261
+ this.semicolon();
2262
+ return /** @type {AST.ExpressionStatement} */ (
2263
+ this.finishNode(node, 'ExpressionStatement')
2264
+ );
2265
+ }
2266
+ }
2267
+
2419
2268
  return super.parseStatement(context, topLevel, exports);
2420
2269
  }
2421
2270
 
@@ -2809,15 +2658,9 @@ function get_comment_handlers(source, comments, index = 0) {
2809
2658
  isSwitchCaseSibling = true;
2810
2659
  } else if (parent.type === 'SwitchCase') {
2811
2660
  node_array = parent.consequent;
2812
- } else if (
2813
- parent.type === 'ArrayExpression' ||
2814
- parent.type === 'RippleArrayExpression'
2815
- ) {
2661
+ } else if (parent.type === 'ArrayExpression') {
2816
2662
  node_array = parent.elements;
2817
- } else if (
2818
- parent.type === 'ObjectExpression' ||
2819
- parent.type === 'RippleObjectExpression'
2820
- ) {
2663
+ } else if (parent.type === 'ObjectExpression') {
2821
2664
  node_array = parent.properties;
2822
2665
  } else if (
2823
2666
  parent.type === 'FunctionDeclaration' ||
@@ -328,6 +328,35 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
328
328
  }
329
329
  }
330
330
 
331
+ /**
332
+ * Checks if a function parameter has a Tracked<T> type annotation imported from ripple.
333
+ * 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
335
+ * @param {AnalysisContext} context - The analysis context
336
+ * @returns {boolean}
337
+ */
338
+ function is_param_tracked_type(param, context) {
339
+ const annotation = param.typeAnnotation?.typeAnnotation;
340
+
341
+ if (
342
+ annotation?.type === 'TSTypeReference' &&
343
+ annotation.typeName?.type === 'Identifier' &&
344
+ annotation.typeName.name === 'Tracked'
345
+ ) {
346
+ const binding = context.state.scope.get('Tracked');
347
+
348
+ return (
349
+ binding?.declaration_kind === 'import' &&
350
+ binding.initial !== null &&
351
+ binding.initial.type === 'ImportDeclaration' &&
352
+ binding.initial.source.type === 'Literal' &&
353
+ binding.initial.source.value === 'ripple'
354
+ );
355
+ }
356
+
357
+ return false;
358
+ }
359
+
331
360
  /**
332
361
  * @param {AST.Function} node
333
362
  * @param {AnalysisContext} context
@@ -345,7 +374,11 @@ function visit_function(node, context) {
345
374
 
346
375
  if ((param.type === 'ObjectPattern' || param.type === 'ArrayPattern') && param.lazy) {
347
376
  const param_id = b.id(context.state.scope.generate('param'));
348
- setup_lazy_transforms(param, param_id, context.state, true, false);
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);
349
382
  // Store the generated identifier name on the pattern for the transform phase
350
383
  param.metadata = { ...param.metadata, lazy_id: param_id.name };
351
384
  }
@@ -579,7 +612,7 @@ const visitors = {
579
612
  }
580
613
 
581
614
  // Lazy bindings from track() calls (read_unwraps) are inherently reactive —
582
- // propagate tracking even without the @ prefix so that control flow (if/for/switch)
615
+ // propagate tracking so that control flow (if/for/switch)
583
616
  // and early returns create reactive blocks
584
617
  if (
585
618
  !node.tracked &&
@@ -673,7 +706,7 @@ const visitors = {
673
706
 
674
707
  if (propertyName && internalProperties.has(propertyName)) {
675
708
  error(
676
- `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`get(${node.object.name})\` or \`@${node.object.name}\` instead.`,
709
+ `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`${node.object.name}.value\` or \`&[]\` lazy destructuring instead.`,
677
710
  context.state.analysis.module.filename,
678
711
  node.property,
679
712
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -684,6 +717,8 @@ const visitors = {
684
717
 
685
718
  if (
686
719
  binding !== null &&
720
+ binding.kind !== 'lazy' &&
721
+ binding.kind !== 'lazy_fallback' &&
687
722
  binding.initial?.type === 'CallExpression' &&
688
723
  is_ripple_track_call(binding.initial.callee, context)
689
724
  ) {
@@ -701,7 +736,7 @@ const visitors = {
701
736
  // pass through
702
737
  } else {
703
738
  error(
704
- `Accessing a tracked object directly is not allowed, use the \`@\` prefix to read the value inside a tracked object - for example \`@${node.object.name}${node.property.type === 'Identifier' ? `.${node.property.name}` : ''}\``,
739
+ `Accessing a tracked object directly is not allowed, use \`.value\` or \`&[]\` lazy destructuring to read the value inside a tracked object - for example \`${node.object.name}.value\``,
705
740
  context.state.analysis.module.filename,
706
741
  node.object,
707
742
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -794,20 +829,6 @@ const visitors = {
794
829
  declarator.id.metadata = { ...declarator.id.metadata, lazy_id: lazy_id.name };
795
830
  }
796
831
 
797
- const paths = extract_paths(declarator.id);
798
-
799
- for (const path of paths) {
800
- if (path.node.tracked) {
801
- error(
802
- 'Variables cannot be reactively referenced using @',
803
- state.analysis.module.filename,
804
- path.node,
805
- context.state.loose ? context.state.analysis.errors : undefined,
806
- context.state.analysis.comments,
807
- );
808
- }
809
- }
810
-
811
832
  visit(declarator, state);
812
833
  }
813
834
 
@@ -815,6 +836,30 @@ const visitors = {
815
836
  }
816
837
  },
817
838
 
839
+ ExpressionStatement(node, context) {
840
+ const { state, visit } = context;
841
+
842
+ // Handle standalone lazy destructuring assignment: &[data] = track(0);
843
+ if (
844
+ node.expression.type === 'AssignmentExpression' &&
845
+ node.expression.operator === '=' &&
846
+ (node.expression.left.type === 'ObjectPattern' ||
847
+ node.expression.left.type === 'ArrayPattern') &&
848
+ node.expression.left.lazy
849
+ ) {
850
+ const pattern = /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node.expression.left);
851
+ const lazy_id = b.id(state.scope.generate('lazy'));
852
+ const init = /** @type {AST.Expression} */ (node.expression.right);
853
+ const init_is_track =
854
+ init?.type === 'CallExpression' && is_ripple_track_call(init.callee, context) === 'track';
855
+ setup_lazy_transforms(pattern, lazy_id, state, true, !!init_is_track);
856
+ // Store the generated identifier name on the pattern for the transform phase
857
+ pattern.metadata = { ...pattern.metadata, lazy_id: lazy_id.name };
858
+ }
859
+
860
+ context.next();
861
+ },
862
+
818
863
  StyleIdentifier(node, context) {
819
864
  const component = is_inside_component(context, true);
820
865
  const parent = context.path.at(-1);
@@ -533,9 +533,6 @@ const visitors = {
533
533
  if (context.state.metadata?.tracking === false) {
534
534
  context.state.metadata.tracking = true;
535
535
  }
536
- if (node.tracked && !binding?.read_unwraps) {
537
- return b.call('_$_.get', build_getter(node, context));
538
- }
539
536
  }
540
537
  return build_getter(node, context);
541
538
  }
@@ -919,6 +916,25 @@ const visitors = {
919
916
  return context.next();
920
917
  },
921
918
 
919
+ ExpressionStatement(node, context) {
920
+ // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
921
+ if (
922
+ node.expression.type === 'AssignmentExpression' &&
923
+ node.expression.left.lazy &&
924
+ node.expression.left.metadata?.lazy_id
925
+ ) {
926
+ if (context.state.to_ts) {
927
+ // In TypeScript mode, convert to a regular assignment (drop the pattern)
928
+ node.expression.left.lazy = false;
929
+ delete node.expression.left.metadata.lazy_id;
930
+ return context.next();
931
+ }
932
+ const right = /** @type {AST.Expression} */ (context.visit(node.expression.right));
933
+ return b.const(b.id(node.expression.left.metadata.lazy_id), right);
934
+ }
935
+ return context.next();
936
+ },
937
+
922
938
  VariableDeclaration(node, context) {
923
939
  for (const declarator of node.declarations) {
924
940
  if (!context.state.to_ts) {
@@ -333,29 +333,7 @@ const visitors = {
333
333
  binding.node !== node &&
334
334
  (binding.kind === 'lazy' || binding.kind === 'lazy_fallback')
335
335
  ) {
336
- const transformed = binding.transform.read(node);
337
- if (node.tracked && !binding.read_unwraps) {
338
- const is_right_side_of_assignment =
339
- parent.type === 'AssignmentExpression' && parent.right === node;
340
- if (
341
- (parent.type !== 'AssignmentExpression' && parent.type !== 'UpdateExpression') ||
342
- is_right_side_of_assignment
343
- ) {
344
- return b.call('_$_.get', transformed);
345
- }
346
- }
347
- return transformed;
348
- }
349
-
350
- if (node.tracked) {
351
- const is_right_side_of_assignment =
352
- parent.type === 'AssignmentExpression' && parent.right === node;
353
- if (
354
- (parent.type !== 'AssignmentExpression' && parent.type !== 'UpdateExpression') ||
355
- is_right_side_of_assignment
356
- ) {
357
- return b.call('_$_.get', node);
358
- }
336
+ return binding.transform.read(node);
359
337
  }
360
338
 
361
339
  return node;
@@ -835,6 +813,19 @@ const visitors = {
835
813
  return statements.length ? b.block(statements) : b.empty;
836
814
  },
837
815
 
816
+ ExpressionStatement(node, context) {
817
+ // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
818
+ if (
819
+ node.expression.type === 'AssignmentExpression' &&
820
+ node.expression.left.lazy &&
821
+ node.expression.left.metadata?.lazy_id
822
+ ) {
823
+ const right = /** @type {AST.Expression} */ (context.visit(node.expression.right));
824
+ return b.const(b.id(node.expression.left.metadata.lazy_id), right);
825
+ }
826
+ return context.next();
827
+ },
828
+
838
829
  VariableDeclaration(node, context) {
839
830
  for (const declarator of node.declarations) {
840
831
  if (!context.state.to_ts) {
@@ -1202,9 +1193,10 @@ const visitors = {
1202
1193
 
1203
1194
  /** @type {AST.Statement[]} */
1204
1195
  const init = [];
1196
+ const visited_id = /** @type {AST.Expression} */ (visit(node.id, state));
1205
1197
  /** @type {AST.Statement[]} */
1206
1198
  const statements = [
1207
- b.const(comp_id, /** @type {AST.Expression} */ (visit(node.id, state))),
1199
+ b.const(comp_id, is_element_dynamic(node) ? b.call('_$_.get', visited_id) : visited_id),
1208
1200
  b.const(args_id, b.array(args)),
1209
1201
  ];
1210
1202
 
@@ -543,8 +543,6 @@ export namespace Parse {
543
543
  */
544
544
  finishToken(type: TokenType, val?: string | number | RegExp | bigint): void;
545
545
 
546
- readAtIdentifier(): void;
547
-
548
546
  /**
549
547
  * Read a token based on character code
550
548
  * Called by nextToken() for each character
@@ -930,8 +928,6 @@ export namespace Parse {
930
928
  | AST.ServerIdentifier
931
929
  | AST.StyleIdentifier
932
930
  | AST.TrackedExpression
933
- | AST.RippleArrayExpression
934
- | AST.RippleObjectExpression
935
931
  | AST.Component
936
932
  | AST.Identifier
937
933
  | AST.Literal;
@@ -954,12 +950,8 @@ export namespace Parse {
954
950
  /** Parse parenthesized expression (just the expression) */
955
951
  parseParenExpression(): AST.Expression;
956
952
 
957
- parseRippleArrayExpression(): AST.RippleArrayExpression;
958
-
959
953
  parseTrackedExpression(): AST.TrackedExpression;
960
954
 
961
- parseRippleObjectExpression(): AST.RippleObjectExpression;
962
-
963
955
  /**
964
956
  * Parse item in parentheses (can be overridden for flow/ts)
965
957
  */
@@ -3,7 +3,7 @@
3
3
  import { branch, destroy_block, render, render_spread } from './blocks.js';
4
4
  import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
5
5
  import { hydrate_next, hydrating } from './hydration.js';
6
- import { active_block, active_namespace, with_ns } from './runtime.js';
6
+ import { active_block, active_namespace, get, with_ns } from './runtime.js';
7
7
  import { top_element_to_ns } from './utils.js';
8
8
 
9
9
  /**
@@ -29,7 +29,7 @@ export function composite(get_component, node, props) {
29
29
 
30
30
  render(
31
31
  () => {
32
- var component = get_component();
32
+ var component = get(get_component());
33
33
 
34
34
  if (b !== null) {
35
35
  destroy_block(b);