ripple 0.3.6 → 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 (110) hide show
  1. package/CHANGELOG.md +43 -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 +290 -26
  5. package/src/compiler/phases/3-transform/client/index.js +54 -14
  6. package/src/compiler/phases/3-transform/server/index.js +19 -35
  7. package/src/compiler/types/index.d.ts +3 -1
  8. package/src/compiler/types/parse.d.ts +0 -8
  9. package/src/compiler/utils.js +10 -6
  10. package/src/runtime/internal/client/composite.js +2 -2
  11. package/src/runtime/internal/client/index.js +2 -0
  12. package/src/runtime/internal/client/runtime.js +95 -45
  13. package/src/runtime/internal/client/types.d.ts +10 -0
  14. package/src/runtime/internal/client/utils.js +12 -0
  15. package/src/runtime/internal/server/index.js +89 -17
  16. package/src/runtime/internal/server/types.d.ts +10 -0
  17. package/src/utils/ast.js +1 -1
  18. package/tests/client/array/array.copy-within.test.ripple +12 -12
  19. package/tests/client/array/array.derived.test.ripple +46 -46
  20. package/tests/client/array/array.iteration.test.ripple +10 -10
  21. package/tests/client/array/array.mutations.test.ripple +20 -20
  22. package/tests/client/array/array.to-methods.test.ripple +6 -6
  23. package/tests/client/async-suspend.test.ripple +5 -5
  24. package/tests/client/basic/basic.attributes.test.ripple +81 -81
  25. package/tests/client/basic/basic.collections.test.ripple +9 -9
  26. package/tests/client/basic/basic.components.test.ripple +28 -28
  27. package/tests/client/basic/basic.errors.test.ripple +18 -18
  28. package/tests/client/basic/basic.events.test.ripple +37 -37
  29. package/tests/client/basic/basic.get-set.test.ripple +6 -6
  30. package/tests/client/basic/basic.reactivity.test.ripple +68 -68
  31. package/tests/client/basic/basic.rendering.test.ripple +19 -19
  32. package/tests/client/basic/basic.utilities.test.ripple +3 -3
  33. package/tests/client/boundaries.test.ripple +12 -12
  34. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +5 -5
  35. package/tests/client/compiler/compiler.assignments.test.ripple +19 -19
  36. package/tests/client/compiler/compiler.basic.test.ripple +44 -15
  37. package/tests/client/compiler/compiler.tracked-access.test.ripple +68 -2
  38. package/tests/client/composite/composite.dynamic-components.test.ripple +9 -9
  39. package/tests/client/composite/composite.props.test.ripple +11 -11
  40. package/tests/client/composite/composite.reactivity.test.ripple +43 -43
  41. package/tests/client/composite/composite.render.test.ripple +3 -3
  42. package/tests/client/computed-properties.test.ripple +4 -4
  43. package/tests/client/date.test.ripple +42 -42
  44. package/tests/client/dynamic-elements.test.ripple +42 -42
  45. package/tests/client/events.test.ripple +70 -70
  46. package/tests/client/for.test.ripple +25 -25
  47. package/tests/client/head.test.ripple +19 -19
  48. package/tests/client/html.test.ripple +3 -3
  49. package/tests/client/input-value.test.ripple +84 -84
  50. package/tests/client/lazy-destructuring.test.ripple +123 -14
  51. package/tests/client/map.test.ripple +16 -16
  52. package/tests/client/media-query.test.ripple +7 -7
  53. package/tests/client/portal.test.ripple +11 -11
  54. package/tests/client/ref.test.ripple +4 -4
  55. package/tests/client/return.test.ripple +52 -52
  56. package/tests/client/set.test.ripple +6 -6
  57. package/tests/client/svg.test.ripple +5 -5
  58. package/tests/client/switch.test.ripple +44 -44
  59. package/tests/client/try.test.ripple +5 -5
  60. package/tests/client/url/url.derived.test.ripple +6 -6
  61. package/tests/client/url-search-params/url-search-params.derived.test.ripple +8 -8
  62. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +10 -10
  63. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +10 -10
  64. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +18 -18
  65. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +2 -2
  66. package/tests/hydration/compiled/client/events.js +25 -25
  67. package/tests/hydration/compiled/client/for.js +70 -66
  68. package/tests/hydration/compiled/client/head.js +25 -25
  69. package/tests/hydration/compiled/client/hmr.js +2 -2
  70. package/tests/hydration/compiled/client/html.js +3 -3
  71. package/tests/hydration/compiled/client/if-children.js +24 -24
  72. package/tests/hydration/compiled/client/if.js +18 -18
  73. package/tests/hydration/compiled/client/mixed-control-flow.js +9 -9
  74. package/tests/hydration/compiled/client/portal.js +3 -3
  75. package/tests/hydration/compiled/client/reactivity.js +16 -16
  76. package/tests/hydration/compiled/client/return.js +40 -40
  77. package/tests/hydration/compiled/client/switch.js +12 -12
  78. package/tests/hydration/compiled/server/events.js +19 -19
  79. package/tests/hydration/compiled/server/for.js +41 -41
  80. package/tests/hydration/compiled/server/head.js +26 -26
  81. package/tests/hydration/compiled/server/hmr.js +2 -2
  82. package/tests/hydration/compiled/server/html.js +2 -2
  83. package/tests/hydration/compiled/server/if-children.js +16 -16
  84. package/tests/hydration/compiled/server/if.js +11 -11
  85. package/tests/hydration/compiled/server/mixed-control-flow.js +6 -6
  86. package/tests/hydration/compiled/server/portal.js +2 -2
  87. package/tests/hydration/compiled/server/reactivity.js +16 -16
  88. package/tests/hydration/compiled/server/return.js +25 -25
  89. package/tests/hydration/compiled/server/switch.js +8 -8
  90. package/tests/hydration/components/events.ripple +25 -25
  91. package/tests/hydration/components/for.ripple +66 -66
  92. package/tests/hydration/components/head.ripple +16 -16
  93. package/tests/hydration/components/hmr.ripple +2 -2
  94. package/tests/hydration/components/html.ripple +3 -3
  95. package/tests/hydration/components/if-children.ripple +24 -24
  96. package/tests/hydration/components/if.ripple +18 -18
  97. package/tests/hydration/components/mixed-control-flow.ripple +9 -9
  98. package/tests/hydration/components/portal.ripple +3 -3
  99. package/tests/hydration/components/reactivity.ripple +16 -16
  100. package/tests/hydration/components/return.ripple +40 -40
  101. package/tests/hydration/components/switch.ripple +20 -20
  102. package/tests/server/await.test.ripple +3 -3
  103. package/tests/server/basic.attributes.test.ripple +34 -34
  104. package/tests/server/basic.components.test.ripple +10 -10
  105. package/tests/server/basic.test.ripple +38 -40
  106. package/tests/server/composite.props.test.ripple +9 -9
  107. package/tests/server/dynamic-elements.test.ripple +13 -12
  108. package/tests/server/head.test.ripple +11 -11
  109. package/tests/server/lazy-destructuring.test.ripple +72 -0
  110. package/types/index.d.ts +7 -2
@@ -76,8 +76,16 @@ function mark_control_flow_has_template(path) {
76
76
  * @param {AST.Identifier} source_id - The identifier to access properties on
77
77
  * @param {AnalysisState} state - The analysis state
78
78
  * @param {boolean} writable - Whether assignments/updates should be supported (let vs const)
79
+ * @param {boolean} is_track_call - Whether the RHS is a Ripple track() call
79
80
  */
80
- function setup_lazy_transforms(pattern, source_id, state, writable) {
81
+ function setup_lazy_transforms(pattern, source_id, state, writable, is_track_call) {
82
+ // For ArrayPattern from track() calls, use direct get/set calls as a fast path
83
+ // instead of going through prototype getters source[0]/source[1]
84
+ if (pattern.type === 'ArrayPattern' && is_track_call) {
85
+ setup_lazy_array_transforms(pattern, source_id, state, writable);
86
+ return;
87
+ }
88
+
81
89
  const paths = extract_paths(pattern);
82
90
 
83
91
  for (const path of paths) {
@@ -141,6 +149,214 @@ function setup_lazy_transforms(pattern, source_id, state, writable) {
141
149
  }
142
150
  }
143
151
 
152
+ /**
153
+ * Set up fast-path transforms for lazy array destructuring of tracked values.
154
+ * For index 0 (the value): uses _$_.get/set/update directly instead of source[0] getters.
155
+ * For index 1 (the tracked ref): returns source directly instead of source[1].
156
+ * @param {AST.ArrayPattern} pattern - The array destructuring pattern
157
+ * @param {AST.Identifier} source_id - The identifier for the tracked value
158
+ * @param {AnalysisState} state - The analysis state
159
+ * @param {boolean} writable - Whether assignments/updates should be supported
160
+ */
161
+ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
162
+ for (let i = 0; i < pattern.elements.length; i++) {
163
+ const element = pattern.elements[i];
164
+ if (!element) continue;
165
+
166
+ // Rest elements — fall back to generic source.slice(i)
167
+ if (element.type === 'RestElement') {
168
+ const rest_paths = extract_paths(pattern);
169
+ for (const path of rest_paths) {
170
+ if (!path.is_rest) continue;
171
+ const name = /** @type {AST.Identifier} */ (path.node).name;
172
+ const binding = state.scope.get(name);
173
+ if (binding !== null) {
174
+ binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
175
+ binding.transform = {
176
+ read: (_) => path.expression(source_id),
177
+ };
178
+ }
179
+ }
180
+ continue;
181
+ }
182
+
183
+ const actual = element.type === 'AssignmentPattern' ? element.left : element;
184
+ const has_fallback = element.type === 'AssignmentPattern';
185
+ const fallback_value = has_fallback
186
+ ? /** @type {AST.AssignmentPattern} */ (element).right
187
+ : null;
188
+
189
+ if (actual.type === 'Identifier' && i <= 1) {
190
+ const name = actual.name;
191
+ const binding = state.scope.get(name);
192
+ if (binding === null) continue;
193
+
194
+ binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
195
+
196
+ if (i === 0) {
197
+ // Fast path for index 0: use _$_.get(source) instead of source[0]
198
+ const read_expr = has_fallback
199
+ ? () => b.call('_$_.fallback', b.call('_$_.get', source_id), fallback_value)
200
+ : () => b.call('_$_.get', source_id);
201
+
202
+ // Signal that read already produces an unwrapped value (calls _$_.get internally)
203
+ binding.read_unwraps = true;
204
+
205
+ binding.transform = {
206
+ read: (_) => read_expr(),
207
+ };
208
+
209
+ if (writable) {
210
+ binding.transform.assign = (_, value) => {
211
+ return b.call('_$_.set', source_id, value);
212
+ };
213
+
214
+ if (has_fallback) {
215
+ binding.transform.update = (node) => {
216
+ const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
217
+ const temp = b.id('_v');
218
+
219
+ if (node.prefix) {
220
+ // ++count: compute new value and set it, return new value
221
+ return b.call(
222
+ b.arrow(
223
+ [],
224
+ b.block([
225
+ b.var(temp, b.binary('+', read_expr(), delta)),
226
+ b.stmt(b.call('_$_.set', source_id, temp)),
227
+ b.return(temp),
228
+ ]),
229
+ ),
230
+ );
231
+ } else {
232
+ // count++: read old value, set new value, return old value
233
+ return b.call(
234
+ b.arrow(
235
+ [],
236
+ b.block([
237
+ b.var(temp, read_expr()),
238
+ b.stmt(b.call('_$_.set', source_id, b.binary('+', temp, delta))),
239
+ b.return(temp),
240
+ ]),
241
+ ),
242
+ );
243
+ }
244
+ };
245
+ } else {
246
+ binding.transform.update = (node) => {
247
+ const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
248
+ const args = [source_id];
249
+ if (node.operator === '--') {
250
+ args.push(b.literal(-1));
251
+ }
252
+ return b.call(fn_name, ...args);
253
+ };
254
+ }
255
+ }
256
+ } else {
257
+ // Fast path for index 1: source itself is the tracked ref
258
+ binding.transform = {
259
+ read: (_) => source_id,
260
+ };
261
+ }
262
+ } else {
263
+ // Nested patterns or indices > 1: fall back to generic source[i] access via extract_paths
264
+ /** @type {(object: AST.Expression) => AST.Expression} */
265
+ const base_expression =
266
+ i === 0
267
+ ? (object) => b.call('_$_.get', object)
268
+ : i === 1
269
+ ? (object) => object
270
+ : (object) => b.member(object, b.literal(i), true);
271
+
272
+ const inner_paths = extract_paths(element);
273
+ for (const path of inner_paths) {
274
+ const name = /** @type {AST.Identifier} */ (path.node).name;
275
+ const binding = state.scope.get(name);
276
+ if (binding === null) continue;
277
+
278
+ binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
279
+
280
+ binding.transform = {
281
+ read: (_) => path.expression(base_expression(source_id)),
282
+ };
283
+
284
+ if (writable) {
285
+ binding.transform.assign = (node, value) => {
286
+ return b.assignment(
287
+ '=',
288
+ /** @type {AST.MemberExpression} */ (
289
+ path.update_expression(base_expression(source_id))
290
+ ),
291
+ value,
292
+ );
293
+ };
294
+
295
+ if (path.has_default_value) {
296
+ binding.transform.update = (node) => {
297
+ const member = path.update_expression(base_expression(source_id));
298
+ const fallback_read = path.expression(base_expression(source_id));
299
+ const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
300
+
301
+ if (node.prefix) {
302
+ return b.assignment('=', member, b.binary('+', fallback_read, delta));
303
+ } else {
304
+ const temp = b.id('_v');
305
+ return b.call(
306
+ b.arrow(
307
+ [],
308
+ b.block([
309
+ b.var(temp, fallback_read),
310
+ b.stmt(b.assignment('=', member, b.binary('+', temp, delta))),
311
+ b.return(temp),
312
+ ]),
313
+ ),
314
+ );
315
+ }
316
+ };
317
+ } else {
318
+ binding.transform.update = (node) =>
319
+ b.update(
320
+ node.operator,
321
+ path.update_expression(base_expression(source_id)),
322
+ node.prefix,
323
+ );
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
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
+
144
360
  /**
145
361
  * @param {AST.Function} node
146
362
  * @param {AnalysisContext} context
@@ -158,7 +374,11 @@ function visit_function(node, context) {
158
374
 
159
375
  if ((param.type === 'ObjectPattern' || param.type === 'ArrayPattern') && param.lazy) {
160
376
  const param_id = b.id(context.state.scope.generate('param'));
161
- setup_lazy_transforms(param, param_id, context.state, true);
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);
162
382
  // Store the generated identifier name on the pattern for the transform phase
163
383
  param.metadata = { ...param.metadata, lazy_id: param_id.name };
164
384
  }
@@ -391,6 +611,21 @@ const visitors = {
391
611
  }
392
612
  }
393
613
 
614
+ // Lazy bindings from track() calls (read_unwraps) are inherently reactive —
615
+ // propagate tracking so that control flow (if/for/switch)
616
+ // and early returns create reactive blocks
617
+ if (
618
+ !node.tracked &&
619
+ binding?.read_unwraps &&
620
+ is_reference(node, /** @type {AST.Node} */ (parent)) &&
621
+ binding.node !== node
622
+ ) {
623
+ mark_as_tracked(context.path);
624
+ if (context.state.metadata?.tracking === false) {
625
+ context.state.metadata.tracking = true;
626
+ }
627
+ }
628
+
394
629
  context.next();
395
630
  },
396
631
 
@@ -471,7 +706,7 @@ const visitors = {
471
706
 
472
707
  if (propertyName && internalProperties.has(propertyName)) {
473
708
  error(
474
- `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.`,
475
710
  context.state.analysis.module.filename,
476
711
  node.property,
477
712
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -482,16 +717,32 @@ const visitors = {
482
717
 
483
718
  if (
484
719
  binding !== null &&
720
+ binding.kind !== 'lazy' &&
721
+ binding.kind !== 'lazy_fallback' &&
485
722
  binding.initial?.type === 'CallExpression' &&
486
723
  is_ripple_track_call(binding.initial.callee, context)
487
724
  ) {
488
- error(
489
- `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}` : ''}\``,
490
- context.state.analysis.module.filename,
491
- node.object,
492
- context.state.loose ? context.state.analysis.errors : undefined,
493
- context.state.analysis.comments,
494
- );
725
+ const is_allowed_tracked_access =
726
+ // Allow [0] and [1] indexed access on tracked objects.
727
+ (node.computed &&
728
+ node.property.type === 'Literal' &&
729
+ (node.property.value === 0 || node.property.value === 1)) ||
730
+ // Allow .value and .length property access on tracked objects.
731
+ (!node.computed &&
732
+ node.property.type === 'Identifier' &&
733
+ (node.property.name === 'value' || node.property.name === 'length'));
734
+
735
+ if (is_allowed_tracked_access) {
736
+ // pass through
737
+ } else {
738
+ error(
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\``,
740
+ context.state.analysis.module.filename,
741
+ node.object,
742
+ context.state.loose ? context.state.analysis.errors : undefined,
743
+ context.state.analysis.comments,
744
+ );
745
+ }
495
746
  }
496
747
  }
497
748
 
@@ -570,25 +821,14 @@ const visitors = {
570
821
  ) {
571
822
  const lazy_id = b.id(state.scope.generate('lazy'));
572
823
  const writable = node.kind !== 'const';
573
- setup_lazy_transforms(declarator.id, lazy_id, state, writable);
824
+ const init_is_track =
825
+ declarator.init?.type === 'CallExpression' &&
826
+ is_ripple_track_call(declarator.init.callee, context) === 'track';
827
+ setup_lazy_transforms(declarator.id, lazy_id, state, writable, !!init_is_track);
574
828
  // Store the generated identifier name on the pattern for the transform phase
575
829
  declarator.id.metadata = { ...declarator.id.metadata, lazy_id: lazy_id.name };
576
830
  }
577
831
 
578
- const paths = extract_paths(declarator.id);
579
-
580
- for (const path of paths) {
581
- if (path.node.tracked) {
582
- error(
583
- 'Variables cannot be reactively referenced using @',
584
- state.analysis.module.filename,
585
- path.node,
586
- context.state.loose ? context.state.analysis.errors : undefined,
587
- context.state.analysis.comments,
588
- );
589
- }
590
- }
591
-
592
832
  visit(declarator, state);
593
833
  }
594
834
 
@@ -596,6 +836,30 @@ const visitors = {
596
836
  }
597
837
  },
598
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
+
599
863
  StyleIdentifier(node, context) {
600
864
  const component = is_inside_component(context, true);
601
865
  const parent = context.path.at(-1);
@@ -651,7 +915,7 @@ const visitors = {
651
915
 
652
916
  if ((props.type === 'ObjectPattern' || props.type === 'ArrayPattern') && props.lazy) {
653
917
  // Lazy destructuring: &{...} or &[...] — set up lazy transforms
654
- setup_lazy_transforms(props, b.id('__props'), context.state, true);
918
+ setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
655
919
  } else if (props.type === 'AssignmentPattern') {
656
920
  error(
657
921
  'Props are always an object, use destructured props with default values instead',
@@ -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) {
537
- return b.call('_$_.get', build_getter(node, context));
538
- }
539
536
  }
540
537
  return build_getter(node, context);
541
538
  }
@@ -634,17 +631,9 @@ const visitors = {
634
631
  }
635
632
  }
636
633
 
637
- if (!context.state.to_ts && is_ripple_track_call(callee, context)) {
638
- const track_method_name =
639
- callee.type === 'Identifier'
640
- ? callee.name === 'trackSplit'
641
- ? 'track_split'
642
- : 'track'
643
- : callee.type === 'MemberExpression' && callee.property.type === 'Identifier'
644
- ? callee.property.name === 'trackSplit'
645
- ? 'track_split'
646
- : 'track'
647
- : 'track';
634
+ const matched_track_call = !context.state.to_ts ? is_ripple_track_call(callee, context) : null;
635
+ if (matched_track_call) {
636
+ const track_method_name = matched_track_call === 'trackSplit' ? 'track_split' : 'track';
648
637
 
649
638
  if (callee.type === 'Identifier' && callee.name === 'track') {
650
639
  if (node.arguments.length === 0) {
@@ -927,6 +916,25 @@ const visitors = {
927
916
  return context.next();
928
917
  },
929
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
+
930
938
  VariableDeclaration(node, context) {
931
939
  for (const declarator of node.declarations) {
932
940
  if (!context.state.to_ts) {
@@ -943,6 +951,20 @@ const visitors = {
943
951
  }
944
952
  }
945
953
 
954
+ if (context.state.to_ts) {
955
+ for (const declarator of node.declarations) {
956
+ if (
957
+ (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') &&
958
+ declarator.id.lazy
959
+ ) {
960
+ declarator.id.lazy = false;
961
+ if (declarator.id.metadata?.lazy_id) {
962
+ delete declarator.id.metadata.lazy_id;
963
+ }
964
+ }
965
+ }
966
+ }
967
+
946
968
  return context.next();
947
969
  },
948
970
 
@@ -2019,6 +2041,24 @@ const visitors = {
2019
2041
 
2020
2042
  const left = node.left;
2021
2043
 
2044
+ // Handle lazy binding assignments (e.g., value = 5 where value is from let &[value] = track(0))
2045
+ // Must come before the left.tracked check to use the binding's transform
2046
+ if (left.type === 'Identifier') {
2047
+ const binding = context.state.scope?.get(left.name);
2048
+ if (binding?.transform?.assign && binding.node !== left) {
2049
+ let value = /** @type {AST.Expression} */ (context.visit(node.right));
2050
+
2051
+ // For compound operators (+=, -=, *=, /=), expand to read + operation
2052
+ if (node.operator !== '=') {
2053
+ const operator = node.operator.slice(0, -1); // '+=' -> '+'
2054
+ const current = binding.transform.read(left);
2055
+ value = b.binary(/** @type {AST.BinaryOperator} */ (operator), current, value);
2056
+ }
2057
+
2058
+ return binding.transform.assign(left, value);
2059
+ }
2060
+ }
2061
+
2022
2062
  if (
2023
2063
  left.type === 'MemberExpression' &&
2024
2064
  (left.tracked || (left.property.type === 'Identifier' && left.property.tracked))
@@ -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) {
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;
@@ -520,17 +498,9 @@ const visitors = {
520
498
  }
521
499
  }
522
500
 
523
- if (is_ripple_track_call(callee, context)) {
524
- const track_method_name =
525
- callee.type === 'Identifier'
526
- ? callee.name === 'trackSplit'
527
- ? 'track_split'
528
- : 'track'
529
- : callee.type === 'MemberExpression' && callee.property.type === 'Identifier'
530
- ? callee.property.name === 'trackSplit'
531
- ? 'track_split'
532
- : 'track'
533
- : 'track';
501
+ const track_call_name = is_ripple_track_call(callee, context);
502
+ if (track_call_name) {
503
+ const track_method_name = track_call_name === 'trackSplit' ? 'track_split' : 'track';
534
504
 
535
505
  return {
536
506
  ...node,
@@ -843,6 +813,19 @@ const visitors = {
843
813
  return statements.length ? b.block(statements) : b.empty;
844
814
  },
845
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
+
846
829
  VariableDeclaration(node, context) {
847
830
  for (const declarator of node.declarations) {
848
831
  if (!context.state.to_ts) {
@@ -1210,9 +1193,10 @@ const visitors = {
1210
1193
 
1211
1194
  /** @type {AST.Statement[]} */
1212
1195
  const init = [];
1196
+ const visited_id = /** @type {AST.Expression} */ (visit(node.id, state));
1213
1197
  /** @type {AST.Statement[]} */
1214
1198
  const statements = [
1215
- 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),
1216
1200
  b.const(args_id, b.array(args)),
1217
1201
  ];
1218
1202
 
@@ -1157,9 +1157,11 @@ export interface Binding {
1157
1157
  /** Transform functions for reading, assigning, and updating this binding */
1158
1158
  transform?: {
1159
1159
  read: (node?: AST.Identifier) => AST.Expression;
1160
- assign?: (node: AST.Pattern, value: AST.Expression) => AST.AssignmentExpression;
1160
+ assign?: (node: AST.Identifier, value: AST.Expression) => AST.Expression;
1161
1161
  update?: (node: AST.UpdateExpression) => AST.Expression;
1162
1162
  };
1163
+ /** Whether the read transform already produces an unwrapped value (calls get() internally) */
1164
+ read_unwraps?: boolean;
1163
1165
  }
1164
1166
 
1165
1167
  /**
@@ -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
  */
@@ -289,27 +289,31 @@ export function is_component_level_function(context) {
289
289
  }
290
290
 
291
291
  /**
292
- * Returns true if callee is a Ripple track call
292
+ * Returns the matched Ripple tracking call name
293
293
  * @param {AST.Expression | AST.Super} callee
294
294
  * @param {CommonContext} context
295
- * @returns {boolean}
295
+ * @returns {'track' | 'trackSplit' | null}
296
296
  */
297
297
  export function is_ripple_track_call(callee, context) {
298
298
  // Super expressions cannot be Ripple track calls
299
- if (callee.type === 'Super') return false;
299
+ if (callee.type === 'Super') return null;
300
300
 
301
301
  if (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'trackSplit')) {
302
- return is_ripple_import(callee, context);
302
+ return is_ripple_import(callee, context) ? callee.name : null;
303
303
  }
304
304
 
305
- return (
305
+ if (
306
306
  callee.type === 'MemberExpression' &&
307
307
  callee.object.type === 'Identifier' &&
308
308
  callee.property.type === 'Identifier' &&
309
309
  (callee.property.name === 'track' || callee.property.name === 'trackSplit') &&
310
310
  !callee.computed &&
311
311
  is_ripple_import(callee, context)
312
- );
312
+ ) {
313
+ return callee.property.name;
314
+ }
315
+
316
+ return null;
313
317
  }
314
318
 
315
319
  /**
@@ -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);
@@ -80,6 +80,8 @@ export { switch_block as switch } from './switch.js';
80
80
 
81
81
  export { template, append, text } from './template.js';
82
82
 
83
+ export { array_slice } from './utils.js';
84
+
83
85
  export { ripple_array } from '../../array.js';
84
86
 
85
87
  export { ripple_object } from '../../object.js';