ripple 0.3.10 → 0.3.11

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 (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/index.d.ts +3 -1
  5. package/src/compiler/phases/1-parse/index.js +170 -8
  6. package/src/compiler/phases/2-analyze/index.js +231 -20
  7. package/src/compiler/phases/3-transform/client/index.js +169 -77
  8. package/src/compiler/phases/3-transform/server/index.js +46 -3
  9. package/src/compiler/types/index.d.ts +19 -2
  10. package/src/compiler/types/parse.d.ts +1 -1
  11. package/src/compiler/utils.js +174 -0
  12. package/src/runtime/index-client.js +14 -4
  13. package/src/runtime/internal/client/composite.js +2 -2
  14. package/src/runtime/internal/client/expression.js +64 -2
  15. package/src/runtime/internal/client/portal.js +1 -1
  16. package/src/utils/builders.js +30 -0
  17. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  18. package/tests/client/basic/basic.rendering.test.ripple +4 -2
  19. package/tests/client/composite/composite.render.test.ripple +46 -0
  20. package/tests/client/return.test.ripple +101 -0
  21. package/tests/client/tsx.test.ripple +486 -0
  22. package/tests/hydration/compiled/client/basic.js +8 -24
  23. package/tests/hydration/compiled/client/composite.js +6 -24
  24. package/tests/hydration/compiled/client/events.js +9 -54
  25. package/tests/hydration/compiled/client/for.js +59 -152
  26. package/tests/hydration/compiled/client/head.js +5 -20
  27. package/tests/hydration/compiled/client/hmr.js +2 -8
  28. package/tests/hydration/compiled/client/html.js +59 -226
  29. package/tests/hydration/compiled/client/if-children.js +6 -22
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +18 -66
  31. package/tests/hydration/compiled/client/nested-control-flow.js +92 -368
  32. package/tests/hydration/compiled/client/portal.js +4 -16
  33. package/tests/hydration/compiled/client/reactivity.js +7 -40
  34. package/tests/hydration/compiled/client/return.js +1 -4
  35. package/tests/hydration/compiled/client/try.js +2 -2
  36. package/tests/utils/compiler-compat-config.test.js +38 -0
  37. package/tests/utils/vite-plugin-config.test.js +113 -0
  38. package/tsconfig.typecheck.json +2 -1
  39. package/types/index.d.ts +2 -12
@@ -61,6 +61,7 @@ import {
61
61
  is_ripple_import,
62
62
  replace_lazy_param_pattern,
63
63
  ripple_import_requires_block,
64
+ jsx_to_ripple_node,
64
65
  } from '../../../utils.js';
65
66
  import {
66
67
  CSS_HASH_IDENTIFIER,
@@ -858,6 +859,8 @@ const visitors = {
858
859
  // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
859
860
  if (
860
861
  node.expression.type === 'AssignmentExpression' &&
862
+ (node.expression.left.type === 'ObjectPattern' ||
863
+ node.expression.left.type === 'ArrayPattern') &&
861
864
  node.expression.left.lazy &&
862
865
  node.expression.left.metadata?.lazy_id
863
866
  ) {
@@ -1140,6 +1143,16 @@ const visitors = {
1140
1143
  TsxCompat(node, context) {
1141
1144
  const { state, visit } = context;
1142
1145
 
1146
+ // to_ts mode: produce a JSX fragment
1147
+ if (state.to_ts) {
1148
+ const children = /** @type {AST.TsxCompat['children']} */ (
1149
+ node.children
1150
+ .map((child) => visit(/** @type {AST.Node} */ (child), state))
1151
+ .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '')
1152
+ );
1153
+ return b.jsx_fragment(children);
1154
+ }
1155
+
1143
1156
  state.template?.push('<!>');
1144
1157
 
1145
1158
  const normalized_children = node.children.filter((child) => {
@@ -1177,6 +1190,58 @@ const visitors = {
1177
1190
  );
1178
1191
  },
1179
1192
 
1193
+ Tsx(node, context) {
1194
+ const { state, visit } = context;
1195
+
1196
+ // to_ts mode: produce a JSX fragment
1197
+ if (state.to_ts) {
1198
+ const children = /** @type {AST.Tsx['children']} */ (
1199
+ node.children
1200
+ .map((child) => visit(/** @type {AST.Node} */ (child), state))
1201
+ .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '')
1202
+ );
1203
+ return b.jsx_fragment(children);
1204
+ }
1205
+
1206
+ const children_filtered = node.children
1207
+ .map((child) => jsx_to_ripple_node(/** @type {AST.Node} */ (child)))
1208
+ .flat()
1209
+ .filter(
1210
+ (child) => child != null && child.type !== 'EmptyStatement' && child.type !== 'Component',
1211
+ );
1212
+
1213
+ const children_component = b.component(b.id('render_children'), [], children_filtered);
1214
+
1215
+ const element = b.call(
1216
+ '_$_.ripple_element',
1217
+ /** @type {AST.Expression} */ (
1218
+ visit(children_component, {
1219
+ ...state,
1220
+ namespace: state.namespace,
1221
+ is_ripple_element: true,
1222
+ })
1223
+ ),
1224
+ );
1225
+
1226
+ // Template body context: push to template and schedule init
1227
+ if (state.flush_node) {
1228
+ state.template?.push('<!>');
1229
+
1230
+ const id = state.flush_node(false);
1231
+
1232
+ const call = b.call('_$_.expression', id, b.thunk(element));
1233
+ state.init?.push(
1234
+ state.namespace !== DEFAULT_NAMESPACE
1235
+ ? b.stmt(b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(call)))
1236
+ : b.stmt(call),
1237
+ );
1238
+ return;
1239
+ }
1240
+
1241
+ // Expression context: return the ripple_element directly as an expression value
1242
+ return element;
1243
+ },
1244
+
1180
1245
  Element(node, context) {
1181
1246
  const { state, visit } = context;
1182
1247
 
@@ -1574,6 +1639,7 @@ const visitors = {
1574
1639
  child.type === 'TryStatement' ||
1575
1640
  child.type === 'ForOfStatement' ||
1576
1641
  child.type === 'SwitchStatement' ||
1642
+ child.type === 'Tsx' ||
1577
1643
  child.type === 'TsxCompat' ||
1578
1644
  child.type === 'Html' ||
1579
1645
  (child.type === 'Element' &&
@@ -1623,7 +1689,9 @@ const visitors = {
1623
1689
  let property =
1624
1690
  attr.value === null
1625
1691
  ? b.literal(true)
1626
- : /** @type {AST.Expression} */ (visit(attr.value, { ...state, metadata }));
1692
+ : /** @type {AST.Expression} */ (
1693
+ visit(attr.value, { ...state, flush_node: null, metadata })
1694
+ );
1627
1695
 
1628
1696
  if (attr.name.name === 'class' && node.metadata.scoped && state.component?.css) {
1629
1697
  if (property.type === 'Literal') {
@@ -1671,7 +1739,9 @@ const visitors = {
1671
1739
  b.prop(
1672
1740
  'init',
1673
1741
  b.key(attr.name.name),
1674
- /** @type {AST.Expression} */ (visit(/** @type {AST.Node} */ (attr.value), state)),
1742
+ /** @type {AST.Expression} */ (
1743
+ visit(/** @type {AST.Node} */ (attr.value), { ...state, flush_node: null })
1744
+ ),
1675
1745
  ),
1676
1746
  );
1677
1747
  }
@@ -1679,7 +1749,13 @@ const visitors = {
1679
1749
  props.push(
1680
1750
  b.spread(
1681
1751
  /** @type {AST.Expression} */
1682
- (visit(attr.argument, { ...state, metadata: { ...state.metadata } })),
1752
+ (
1753
+ visit(attr.argument, {
1754
+ ...state,
1755
+ flush_node: null,
1756
+ metadata: { ...state.metadata },
1757
+ })
1758
+ ),
1683
1759
  ),
1684
1760
  );
1685
1761
  } else if (attr.type === 'RefAttribute') {
@@ -1690,7 +1766,9 @@ const visitors = {
1690
1766
  b.prop(
1691
1767
  'init',
1692
1768
  b.id(ref_id),
1693
- /** @type {AST.Expression} */ (visit(attr.argument, { ...state, metadata })),
1769
+ /** @type {AST.Expression} */ (
1770
+ visit(attr.argument, { ...state, flush_node: null, metadata })
1771
+ ),
1694
1772
  true,
1695
1773
  ),
1696
1774
  );
@@ -1736,6 +1814,7 @@ const visitors = {
1736
1814
  : {}),
1737
1815
  scope: /** @type {ScopeInterface} */ (component_scope),
1738
1816
  namespace: child_namespace,
1817
+ is_ripple_element: true,
1739
1818
  })
1740
1819
  ),
1741
1820
  );
@@ -1953,30 +2032,46 @@ const visitors = {
1953
2032
  }
1954
2033
 
1955
2034
  const component_scope = context.state.scopes.get(node) || context.state.scope;
1956
- const body_statements = [
1957
- b.stmt(b.call('_$_.push_component')),
1958
- ...transform_body(node.body, {
1959
- ...context,
1960
- state: {
1961
- ...context.state,
1962
- flush_node: null,
1963
- component: node,
1964
- metadata,
1965
- scope: component_scope,
1966
- },
1967
- }),
1968
- b.stmt(b.call('_$_.pop_component')),
1969
- ];
2035
+ const is_ripple_element = context.state.is_ripple_element;
2036
+ const is_synthetic_children = node.id?.name === 'render_children';
2037
+ const transformed_body = transform_body(node.body, {
2038
+ ...context,
2039
+ state: {
2040
+ ...context.state,
2041
+ flush_node: null,
2042
+ component: node,
2043
+ metadata,
2044
+ scope: component_scope,
2045
+ is_ripple_element: false,
2046
+ applyParentCssScope: is_synthetic_children ? context.state.applyParentCssScope : undefined,
2047
+ },
2048
+ });
2049
+
2050
+ // RippleElement render functions don't need push/pop component context
2051
+ // since they inherit context from where they're used
2052
+ const body_statements = is_ripple_element
2053
+ ? transformed_body
2054
+ : [
2055
+ b.stmt(b.call('_$_.push_component')),
2056
+ ...transformed_body,
2057
+ b.stmt(b.call('_$_.pop_component')),
2058
+ ];
1970
2059
 
1971
2060
  if (node.css !== null && node.css) {
1972
2061
  context.state.stylesheets.push(node.css);
1973
2062
  }
1974
2063
 
2064
+ // RippleElement render functions use simpler params: [__anchor, __block]
2065
+ // Regular components use: [__anchor, props, __block] or [__anchor, _, __block]
2066
+ const params = is_ripple_element
2067
+ ? [b.id('__anchor'), b.id('__block')]
2068
+ : node.params.length > 0
2069
+ ? [b.id('__anchor'), props, b.id('__block')]
2070
+ : [b.id('__anchor'), b.id('_'), b.id('__block')];
2071
+
1975
2072
  const func = b.function(
1976
2073
  node.id,
1977
- node.params.length > 0
1978
- ? [b.id('__anchor'), props, b.id('__block')]
1979
- : [b.id('__anchor'), b.id('_'), b.id('__block')],
2074
+ params,
1980
2075
  b.block([
1981
2076
  ...style_statements,
1982
2077
  ...(prop_statements ?? []),
@@ -2821,7 +2916,10 @@ function transform_ts_child(node, context) {
2821
2916
  }
2822
2917
  }
2823
2918
 
2824
- if (/** @type {AST.Node} */ (node.id).type !== 'MemberExpression' && node.id.tracked) {
2919
+ if (
2920
+ /** @type {AST.Node} */ (node.id).type !== 'MemberExpression' &&
2921
+ /** @type {AST.Identifier} */ (node.id).tracked
2922
+ ) {
2825
2923
  // This is just temporary until we remove capitalization
2826
2924
  // The `is_capitalized` was never handled for MemberExpression
2827
2925
  // but it should've been for the `object` part because it starts the tag
@@ -3064,6 +3162,18 @@ function transform_ts_child(node, context) {
3064
3162
  );
3065
3163
 
3066
3164
  state.init?.push(b.stmt(b.jsx_fragment(children)));
3165
+ } else if (node.type === 'Tsx') {
3166
+ const children = /** @type {AST.Tsx['children']} */ (
3167
+ node.children
3168
+ .map((child) => visit(/** @type {AST.Node} */ (child), state))
3169
+ .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '')
3170
+ );
3171
+
3172
+ const result = b.jsx_fragment(children);
3173
+ if (!state.init) {
3174
+ return result;
3175
+ }
3176
+ state.init.push(b.stmt(result));
3067
3177
  } else if (node.type === 'JSXExpressionContainer') {
3068
3178
  // JSX comments {/* ... */} are JSXExpressionContainer with JSXEmptyExpression
3069
3179
  // These should be preserved in the output as-is for prettier to handle
@@ -3105,6 +3215,7 @@ function is_template_or_control_flow(node) {
3105
3215
  node.type === 'RippleExpression' ||
3106
3216
  node.type === 'Text' ||
3107
3217
  node.type === 'Html' ||
3218
+ node.type === 'Tsx' ||
3108
3219
  node.type === 'TsxCompat' ||
3109
3220
  node.type === 'IfStatement' ||
3110
3221
  node.type === 'ForOfStatement' ||
@@ -3196,6 +3307,7 @@ function element_has_dynamic_content(element) {
3196
3307
  child.type === 'TryStatement' ||
3197
3308
  child.type === 'ForOfStatement' ||
3198
3309
  child.type === 'SwitchStatement' ||
3310
+ child.type === 'Tsx' ||
3199
3311
  child.type === 'TsxCompat' ||
3200
3312
  child.type === 'Html'
3201
3313
  ) {
@@ -3371,6 +3483,7 @@ function transform_children(children, context) {
3371
3483
  node.type === 'TryStatement' ||
3372
3484
  node.type === 'ForOfStatement' ||
3373
3485
  node.type === 'SwitchStatement' ||
3486
+ node.type === 'Tsx' ||
3374
3487
  node.type === 'TsxCompat' ||
3375
3488
  node.type === 'Html' ||
3376
3489
  (node.type === 'Element' &&
@@ -3384,6 +3497,15 @@ function transform_children(children, context) {
3384
3497
  node.type === 'RippleExpression' &&
3385
3498
  is_children_template_expression(node.expression, state.scope),
3386
3499
  )) ||
3500
+ // At root level, non-literal expressions need a fragment template so the
3501
+ // anchor has a parent node. Without a parent, expression()'s .before() call
3502
+ // is a no-op when the value is a RippleElement.
3503
+ (root &&
3504
+ normalized.some(
3505
+ (node) =>
3506
+ node.type === 'RippleExpression' &&
3507
+ /** @type {AST.RippleExpression} */ (node).expression.type !== 'Literal',
3508
+ )) ||
3387
3509
  normalized.filter(
3388
3510
  (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3389
3511
  ).length > 1;
@@ -3652,6 +3774,7 @@ function transform_children(children, context) {
3652
3774
  child.type === 'TryStatement' ||
3653
3775
  child.type === 'ForOfStatement' ||
3654
3776
  child.type === 'SwitchStatement' ||
3777
+ child.type === 'Tsx' ||
3655
3778
  child.type === 'TsxCompat' ||
3656
3779
  child.type === 'Html' ||
3657
3780
  (child.type === 'Element' &&
@@ -3682,6 +3805,7 @@ function transform_children(children, context) {
3682
3805
  next_node.type === 'TryStatement' ||
3683
3806
  next_node.type === 'ForOfStatement' ||
3684
3807
  next_node.type === 'SwitchStatement' ||
3808
+ next_node.type === 'Tsx' ||
3685
3809
  next_node.type === 'TsxCompat'
3686
3810
  ) {
3687
3811
  needs_sibling_call = true;
@@ -3693,7 +3817,7 @@ function transform_children(children, context) {
3693
3817
  }
3694
3818
  }
3695
3819
  }
3696
- } else if (node.type === 'TsxCompat') {
3820
+ } else if (node.type === 'TsxCompat' || node.type === 'Tsx') {
3697
3821
  skipped = 0;
3698
3822
 
3699
3823
  visit(node, {
@@ -3721,10 +3845,6 @@ function transform_children(children, context) {
3721
3845
  });
3722
3846
  } else if (node.type === 'RippleExpression') {
3723
3847
  const expr = /** @type {AST.Expression} */ (expression);
3724
- const is_children_expression = is_children_template_expression(
3725
- node.expression,
3726
- state.scope,
3727
- );
3728
3848
 
3729
3849
  if (expr.type === 'Literal') {
3730
3850
  if (normalized.length === 1) {
@@ -3743,60 +3863,29 @@ function transform_children(children, context) {
3743
3863
  skipped++;
3744
3864
  state.template?.push(escape_html(expr.value));
3745
3865
  }
3746
- } else if (is_children_expression) {
3747
- skipped = 0;
3748
- state.template?.push('<!>');
3749
- const id = flush_node(false);
3750
- state.update?.push({
3751
- operation: () => {
3752
- const call = b.call('_$_.expression', id, b.thunk(expr));
3753
- return state.namespace !== DEFAULT_NAMESPACE
3754
- ? b.stmt(b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(call)))
3755
- : b.stmt(call);
3756
- },
3757
- });
3758
- if (metadata?.await) {
3759
- /** @type {NonNullable<TransformClientState['update']>} */ (state.update).async = true;
3760
- }
3761
- } else if (metadata?.tracking) {
3762
- skipped = 0;
3763
- state.template?.push(' ');
3764
- const id = flush_node(true);
3765
- state.update?.push({
3766
- operation: (key) => b.stmt(b.call('_$_.set_text', id, key)),
3767
- expression: expr,
3768
- identity: node.expression,
3769
- initial: b.literal(' '),
3770
- });
3771
- if (metadata.await) {
3772
- /** @type {NonNullable<TransformClientState['update']>} */ (state.update).async = true;
3773
- }
3774
- } else if (normalized.length === 1) {
3866
+ } else if (
3867
+ normalized.length === 1 &&
3868
+ !is_children_template_expression(node.expression, state.scope)
3869
+ ) {
3775
3870
  skipped++;
3776
- const id = flush_node(true);
3777
3871
  state.template?.push(' ');
3872
+ const id = flush_node(true);
3873
+ const call = b.call('_$_.expression', id, b.thunk(expr));
3778
3874
  state.init?.push(
3779
- b.stmt(
3780
- b.assignment(
3781
- '=',
3782
- b.member(/** @type {AST.Identifier} */ (id), b.id('nodeValue')),
3783
- expr,
3784
- ),
3785
- ),
3875
+ state.namespace !== DEFAULT_NAMESPACE
3876
+ ? b.stmt(b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(call)))
3877
+ : b.stmt(call),
3786
3878
  );
3787
3879
  } else {
3788
- skipped++;
3789
- state.template?.push(' ');
3790
- const id = flush_node(true);
3791
- state.update?.push({
3792
- operation: (key) => b.stmt(b.call('_$_.set_text', id, key)),
3793
- expression: expr,
3794
- identity: node.expression,
3795
- initial: b.literal(' '),
3796
- });
3797
- if (metadata?.await) {
3798
- /** @type {NonNullable<TransformClientState['update']>} */ (state.update).async = true;
3799
- }
3880
+ skipped = 0;
3881
+ state.template?.push('<!>');
3882
+ const id = flush_node(false);
3883
+ const call = b.call('_$_.expression', id, b.thunk(expr));
3884
+ state.init?.push(
3885
+ state.namespace !== DEFAULT_NAMESPACE
3886
+ ? b.stmt(b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(call)))
3887
+ : b.stmt(call),
3888
+ );
3800
3889
  }
3801
3890
  } else if (node.type === 'Text') {
3802
3891
  if (metadata?.tracking) {
@@ -4399,7 +4488,10 @@ function create_tsx_with_typescript_support(comments) {
4399
4488
  // Shorthand object properties require an Identifier value. When the
4400
4489
  // transformed value is a tracked MemberExpression (for example
4401
4490
  // @value), emit longhand to keep valid output.
4402
- if (node.value.type === 'MemberExpression' && node.value.tracked) {
4491
+ if (
4492
+ node.value.type === 'MemberExpression' &&
4493
+ /** @type {AST.MemberExpression & { tracked?: boolean }} */ (node.value).tracked
4494
+ ) {
4403
4495
  context.visit(node.key);
4404
4496
  context.write(': ');
4405
4497
  context.visit(node.value);
@@ -32,6 +32,7 @@ import {
32
32
  hash,
33
33
  flatten_switch_consequent,
34
34
  get_ripple_namespace_call_name,
35
+ jsx_to_ripple_node,
35
36
  } from '../../../utils.js';
36
37
  import { escape } from '../../../../utils/escaping.js';
37
38
  import { is_event_attribute } from '../../../../utils/events.js';
@@ -55,6 +56,7 @@ function is_template_or_control_flow(node) {
55
56
  node.type === 'RippleExpression' ||
56
57
  node.type === 'Text' ||
57
58
  node.type === 'Html' ||
59
+ node.type === 'Tsx' ||
58
60
  node.type === 'TsxCompat' ||
59
61
  node.type === 'IfStatement' ||
60
62
  node.type === 'ForOfStatement' ||
@@ -199,7 +201,7 @@ function transform_children(children, context) {
199
201
  }
200
202
  }
201
203
  } else {
202
- visit(node, { ...state, return_flags });
204
+ visit(node, { ...state, return_flags, template_child: true });
203
205
  }
204
206
  };
205
207
 
@@ -402,14 +404,22 @@ const visitors = {
402
404
  b.stmt(b.call('_$_.push_component')),
403
405
  ...transform_body(node.body, {
404
406
  ...context,
405
- state: { ...context.state, component: node, metadata },
407
+ state: {
408
+ ...context.state,
409
+ component: node,
410
+ metadata,
411
+ applyParentCssScope:
412
+ node.id?.name === 'render_children' ? context.state.applyParentCssScope : undefined,
413
+ },
406
414
  }),
407
415
  b.stmt(b.call('_$_.pop_component')),
408
416
  );
409
417
 
410
418
  let component_fn = b.function(
411
419
  node.id,
412
- node.params.length > 0 ? [b.id('__output'), props_param_output] : [b.id('__output')],
420
+ node.params.length > 0
421
+ ? [b.id('__output'), /** @type {AST.Pattern} */ (props_param_output)]
422
+ : [b.id('__output')],
413
423
  b.block([
414
424
  ...(metadata.await
415
425
  ? [b.return(b.call('_$_.async', b.thunk(b.block(body_statements), true)))]
@@ -807,6 +817,8 @@ const visitors = {
807
817
  // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
808
818
  if (
809
819
  node.expression.type === 'AssignmentExpression' &&
820
+ (node.expression.left.type === 'ObjectPattern' ||
821
+ node.expression.left.type === 'ArrayPattern') &&
810
822
  node.expression.left.lazy &&
811
823
  node.expression.left.metadata?.lazy_id
812
824
  ) {
@@ -1654,6 +1666,37 @@ const visitors = {
1654
1666
  }
1655
1667
  },
1656
1668
 
1669
+ Tsx(node, { visit, state }) {
1670
+ const converted_children = node.children
1671
+ .map((child) => jsx_to_ripple_node(/** @type {AST.Node} */ (child)))
1672
+ .flat()
1673
+ .filter((child) => child != null);
1674
+
1675
+ /** @type {AST.Statement[]} */
1676
+ const init = [];
1677
+ transform_children(
1678
+ converted_children,
1679
+ /** @type {TransformServerContext} */ ({
1680
+ visit,
1681
+ state: {
1682
+ ...state,
1683
+ init,
1684
+ },
1685
+ }),
1686
+ );
1687
+
1688
+ if (state.template_child) {
1689
+ // Template body: push children statements inline
1690
+ if (init.length > 0) {
1691
+ state.init?.push(b.block(init));
1692
+ }
1693
+ } else {
1694
+ // Expression context: return ripple_element(render_fn)
1695
+ const render_fn = b.function(b.id('render_children'), [b.id('__output')], b.block(init));
1696
+ return b.call('_$_.ripple_element', render_fn);
1697
+ }
1698
+ },
1699
+
1657
1700
  Html(node, { visit, state }) {
1658
1701
  const metadata = { await: false };
1659
1702
  const expression = /** @type {AST.Expression} */ (
@@ -22,6 +22,7 @@ interface BaseNodeMetaData {
22
22
  inside_component_top_level?: boolean;
23
23
  returns?: AST.ReturnStatement[];
24
24
  has_return?: boolean;
25
+ has_throw?: boolean;
25
26
  is_reactive?: boolean;
26
27
  lone_return?: boolean;
27
28
  forceMapping?: boolean;
@@ -116,7 +117,9 @@ declare module 'estree' {
116
117
 
117
118
  // We mark the whole node as marked when member is @[expression]
118
119
  // Otherwise, we only mark Identifier nodes
119
- interface MemberExpression {}
120
+ interface MemberExpression {
121
+ tracked?: boolean;
122
+ }
120
123
 
121
124
  interface SimpleLiteral extends AST.LiteralNode {}
122
125
  interface RegExpLiteral extends AST.LiteralNode {}
@@ -133,6 +136,7 @@ declare module 'estree' {
133
136
  // Include TypeScript node types and Ripple-specific nodes in NodeMap
134
137
  interface NodeMap {
135
138
  Component: Component;
139
+ Tsx: Tsx;
136
140
  TsxCompat: TsxCompat;
137
141
  RippleExpression: RippleExpression;
138
142
  Html: Html;
@@ -270,6 +274,16 @@ declare module 'estree' {
270
274
  typeParameters?: AST.TSTypeParameterDeclaration;
271
275
  }
272
276
 
277
+ interface Tsx extends AST.BaseNode {
278
+ type: 'Tsx';
279
+ attributes: Array<any>;
280
+ children: ESTreeJSX.JSXElement['children'];
281
+ selfClosing?: boolean;
282
+ unclosed?: boolean;
283
+ openingElement: ESTreeJSX.JSXOpeningElement;
284
+ closingElement: ESTreeJSX.JSXClosingElement;
285
+ }
286
+
273
287
  interface TsxCompat extends AST.BaseNode {
274
288
  type: 'TsxCompat';
275
289
  kind: string;
@@ -406,7 +420,7 @@ declare module 'estree' {
406
420
 
407
421
  export type RippleStatement = AST.Statement | TSESTree.Statement;
408
422
 
409
- export type NodeWithChildren = AST.Element | AST.TsxCompat;
423
+ export type NodeWithChildren = AST.Element | AST.Tsx | AST.TsxCompat;
410
424
 
411
425
  export namespace CSS {
412
426
  export interface BaseNode extends AST.NodeWithMaybeComments {
@@ -1270,6 +1284,7 @@ export interface AnalysisState extends BaseState {
1270
1284
  elements?: AST.Element[];
1271
1285
  function_depth?: number;
1272
1286
  loose?: boolean;
1287
+ configured_compat_kinds?: Set<string>;
1273
1288
  metadata: BaseStateMetaData & {
1274
1289
  styleClasses?: StyleClasses;
1275
1290
  };
@@ -1290,6 +1305,7 @@ export interface TransformServerState extends BaseState {
1290
1305
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1291
1306
  dev?: boolean;
1292
1307
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1308
+ template_child?: boolean;
1293
1309
  }
1294
1310
 
1295
1311
  type UpdateList = Array<
@@ -1323,6 +1339,7 @@ export interface TransformClientState extends BaseState {
1323
1339
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1324
1340
  skip_children_traversal: boolean;
1325
1341
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1342
+ is_ripple_element?: boolean;
1326
1343
  }
1327
1344
 
1328
1345
  /** Override zimmerframe types and provide our own */
@@ -1166,7 +1166,7 @@ export namespace Parse {
1166
1166
 
1167
1167
  parseServerBlock(): AST.ServerBlock;
1168
1168
 
1169
- parseElement(): AST.Element | AST.TsxCompat;
1169
+ parseElement(): AST.Element | AST.Tsx | AST.TsxCompat;
1170
1170
 
1171
1171
  parseTemplateBody(
1172
1172
  body: (AST.Statement | AST.Node | ESTreeJSX.JSXText | ESTreeJSX.JSXElement['children'])[],