ripple 0.3.9 → 0.3.10

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 (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +25 -15
  4. package/src/compiler/phases/2-analyze/index.js +35 -88
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +188 -56
  7. package/src/compiler/phases/3-transform/server/index.js +62 -40
  8. package/src/compiler/types/index.d.ts +9 -1
  9. package/src/compiler/types/parse.d.ts +2 -0
  10. package/src/compiler/utils.js +101 -1
  11. package/src/runtime/element.js +39 -0
  12. package/src/runtime/internal/client/composite.js +10 -6
  13. package/src/runtime/internal/client/expression.js +218 -0
  14. package/src/runtime/internal/client/index.js +4 -0
  15. package/src/runtime/internal/client/portal.js +12 -6
  16. package/src/runtime/internal/server/index.js +26 -1
  17. package/tests/client/basic/basic.components.test.ripple +85 -87
  18. package/tests/client/basic/basic.errors.test.ripple +4 -8
  19. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  20. package/tests/client/capture-error.js +12 -0
  21. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  22. package/tests/client/composite/composite.props.test.ripple +1 -3
  23. package/tests/client/composite/composite.render.test.ripple +45 -13
  24. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  25. package/tests/client/svg.test.ripple +4 -4
  26. package/tests/hydration/basic.test.js +23 -0
  27. package/tests/hydration/compiled/client/basic.js +118 -66
  28. package/tests/hydration/compiled/client/composite.js +90 -37
  29. package/tests/hydration/compiled/client/events.js +18 -18
  30. package/tests/hydration/compiled/client/for.js +62 -62
  31. package/tests/hydration/compiled/client/head.js +10 -10
  32. package/tests/hydration/compiled/client/hmr.js +13 -10
  33. package/tests/hydration/compiled/client/html.js +274 -236
  34. package/tests/hydration/compiled/client/if-children.js +41 -35
  35. package/tests/hydration/compiled/client/if.js +2 -2
  36. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  37. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  38. package/tests/hydration/compiled/client/portal.js +8 -8
  39. package/tests/hydration/compiled/client/reactivity.js +14 -14
  40. package/tests/hydration/compiled/client/return.js +2 -2
  41. package/tests/hydration/compiled/client/try.js +4 -4
  42. package/tests/hydration/compiled/server/basic.js +64 -31
  43. package/tests/hydration/compiled/server/composite.js +62 -29
  44. package/tests/hydration/compiled/server/hmr.js +24 -37
  45. package/tests/hydration/compiled/server/html.js +472 -611
  46. package/tests/hydration/compiled/server/if-children.js +77 -103
  47. package/tests/hydration/compiled/server/portal.js +8 -8
  48. package/tests/hydration/components/basic.ripple +15 -5
  49. package/tests/hydration/components/composite.ripple +13 -1
  50. package/tests/hydration/components/hmr.ripple +1 -3
  51. package/tests/hydration/components/html.ripple +13 -35
  52. package/tests/hydration/components/if-children.ripple +4 -8
  53. package/tests/hydration/composite.test.js +11 -0
  54. package/tests/server/basic.attributes.test.ripple +50 -0
  55. package/tests/server/basic.components.test.ripple +22 -28
  56. package/tests/server/basic.test.ripple +12 -0
  57. package/tests/server/compiler.test.ripple +25 -8
  58. package/tests/server/composite.props.test.ripple +1 -3
  59. package/tests/server/style-identifier.test.ripple +2 -4
  60. package/types/index.d.ts +9 -2
@@ -53,6 +53,7 @@ import {
53
53
  determine_namespace_for_children,
54
54
  index_to_key,
55
55
  is_element_dynamic,
56
+ is_children_template_expression,
56
57
  is_inside_left_side_assignment,
57
58
  hash,
58
59
  flatten_switch_consequent,
@@ -1577,7 +1578,8 @@ const visitors = {
1577
1578
  child.type === 'Html' ||
1578
1579
  (child.type === 'Element' &&
1579
1580
  (child.id.type !== 'Identifier' || !is_element_dom_element(child))) ||
1580
- (child.type === 'Text' && child.expression.type !== 'Literal'),
1581
+ ((child.type === 'RippleExpression' || child.type === 'Text') &&
1582
+ child.expression.type !== 'Literal'),
1581
1583
  );
1582
1584
 
1583
1585
  if (needs_pop) {
@@ -1611,7 +1613,7 @@ const visitors = {
1611
1613
  const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
1612
1614
  /** @type {(AST.Property | AST.SpreadElement)[]} */
1613
1615
  const props = [];
1614
- /** @type {AST.Expression | AST.BlockStatement | null} */
1616
+ /** @type {AST.Property | null} */
1615
1617
  let children_prop = null;
1616
1618
 
1617
1619
  for (const attr of node.attributes) {
@@ -1633,7 +1635,15 @@ const visitors = {
1633
1635
 
1634
1636
  if (metadata.tracking || attr.name.tracked) {
1635
1637
  if (attr.name.name === 'children') {
1636
- children_prop = b.thunk(property);
1638
+ children_prop = b.prop(
1639
+ 'get',
1640
+ b.id('children'),
1641
+ b.function(
1642
+ null,
1643
+ [],
1644
+ b.block([b.return(b.call('_$_.normalize_children', property))]),
1645
+ ),
1646
+ );
1637
1647
  continue;
1638
1648
  }
1639
1649
 
@@ -1645,6 +1655,15 @@ const visitors = {
1645
1655
  ),
1646
1656
  );
1647
1657
  } else {
1658
+ if (attr.name.name === 'children') {
1659
+ children_prop = b.prop(
1660
+ 'init',
1661
+ b.id('children'),
1662
+ b.call('_$_.normalize_children', property),
1663
+ );
1664
+ continue;
1665
+ }
1666
+
1648
1667
  props.push(b.prop('init', b.key(attr.name.name), property));
1649
1668
  }
1650
1669
  } else {
@@ -1694,60 +1713,62 @@ const visitors = {
1694
1713
  }
1695
1714
  }
1696
1715
 
1697
- const children_filtered = [];
1698
-
1699
- for (const child of node.children) {
1700
- if (child.type === 'Component') {
1701
- // in this case, id cannot be null
1702
- // as these are direct children of the component
1703
- const id = /** @type {AST.Identifier} */ (child.id);
1704
- props.push(
1705
- b.prop(
1706
- 'init',
1707
- id,
1708
- /** @type {AST.Expression} */ (
1709
- visit(child, { ...state, namespace: child_namespace })
1710
- ),
1711
- ),
1712
- );
1713
- } else {
1714
- children_filtered.push(child);
1715
- }
1716
- }
1716
+ const children_filtered = node.children.filter(
1717
+ (child) => child.type !== 'EmptyStatement' && child.type !== 'Component',
1718
+ );
1717
1719
 
1718
1720
  if (children_filtered.length > 0) {
1719
1721
  const component_scope = state.scopes.get(node);
1720
- const children_component = b.component(b.id('children'), [], children_filtered);
1721
-
1722
- const children = /** @type {AST.Expression} */ (
1723
- visit(children_component, {
1724
- ...state,
1725
- ...(apply_parent_css_scope ||
1726
- (is_dynamic_element && node.metadata.scoped && state.component?.css)
1727
- ? {
1728
- applyParentCssScope:
1729
- apply_parent_css_scope ||
1730
- /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1731
- }
1732
- : {}),
1733
- scope: /** @type {ScopeInterface} */ (component_scope),
1734
- namespace: child_namespace,
1735
- })
1722
+ const children_component = b.component(b.id('render_children'), [], children_filtered);
1723
+
1724
+ const children = b.call(
1725
+ '_$_.ripple_element',
1726
+ /** @type {AST.Expression} */ (
1727
+ visit(children_component, {
1728
+ ...state,
1729
+ ...(apply_parent_css_scope ||
1730
+ (is_dynamic_element && node.metadata.scoped && state.component?.css)
1731
+ ? {
1732
+ applyParentCssScope:
1733
+ apply_parent_css_scope ||
1734
+ /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1735
+ }
1736
+ : {}),
1737
+ scope: /** @type {ScopeInterface} */ (component_scope),
1738
+ namespace: child_namespace,
1739
+ })
1740
+ ),
1736
1741
  );
1737
1742
 
1738
1743
  if (children_prop) {
1739
- /** @type {AST.ArrowFunctionExpression} */ (children_prop).body = b.logical(
1740
- '??',
1741
- /** @type {AST.Expression} */ (
1742
- /** @type {AST.ArrowFunctionExpression} */ (children_prop).body
1743
- ),
1744
- children,
1745
- );
1744
+ if (children_prop.kind === 'get') {
1745
+ /** @type {AST.ReturnStatement} */ (
1746
+ /** @type {AST.FunctionExpression} */ (children_prop.value).body.body[0]
1747
+ ).argument = b.logical(
1748
+ '??',
1749
+ /** @type {AST.Expression} */ (
1750
+ /** @type {AST.ReturnStatement} */ (
1751
+ /** @type {AST.FunctionExpression} */ (children_prop.value).body.body[0]
1752
+ ).argument
1753
+ ),
1754
+ children,
1755
+ );
1756
+ } else {
1757
+ children_prop.value = b.logical(
1758
+ '??',
1759
+ /** @type {AST.Expression} */ (children_prop.value),
1760
+ children,
1761
+ );
1762
+ }
1746
1763
  } else {
1747
- props.push(b.prop('init', b.id('children'), children));
1764
+ children_prop = b.prop('init', b.id('children'), children);
1748
1765
  }
1749
1766
  }
1750
1767
 
1768
+ if (children_prop) {
1769
+ props.push(children_prop);
1770
+ }
1771
+
1751
1772
  const metadata = { tracking: false, await: false };
1752
1773
  // We visit, but only to gather metadata
1753
1774
  b.call(/** @type {AST.Expression} */ (visit(node.id, { ...state, metadata })));
@@ -2638,7 +2659,7 @@ function join_template(items) {
2638
2659
  function transform_ts_child(node, context) {
2639
2660
  const { state, visit } = context;
2640
2661
 
2641
- if (node.type === 'Text') {
2662
+ if (node.type === 'RippleExpression' || node.type === 'Text') {
2642
2663
  state.init?.push(b.stmt(/** @type {AST.Expression} */ (visit(node.expression, { ...state }))));
2643
2664
  } else if (node.type === 'Html') {
2644
2665
  // Do we need to do something special here?
@@ -2805,12 +2826,29 @@ function transform_ts_child(node, context) {
2805
2826
  // The `is_capitalized` was never handled for MemberExpression
2806
2827
  // but it should've been for the `object` part because it starts the tag
2807
2828
  // But the plan is to only rely on source_name and creating a const for the tag with ['#v']
2829
+ const source_name = /** @type {AST.Identifier} */ (node.id).name;
2830
+ const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
2831
+
2832
+ // node.id and node.openingElement.name are the SAME object (convert_from_jsx mutates
2833
+ // the JSXIdentifier to an Identifier in-place). Capitalize the name directly so that
2834
+ // the generated JSX uses <Tag> (uppercase) matching the capitalized variable declaration,
2835
+ // preventing the TypeScript "declared but never read" false-negative (ts6133).
2836
+ /** @type {AST.Identifier} */ (node.id).name = capitalized_name;
2837
+ if (!node.id.metadata) node.id.metadata = /** @type {any} */ ({});
2838
+ node.id.metadata.is_capitalized = true;
2839
+ node.id.metadata.source_name = source_name;
2840
+
2808
2841
  node.openingElement.metadata = {
2809
2842
  ...node.openingElement.metadata,
2810
2843
  is_capitalized: true,
2811
2844
  };
2812
2845
 
2813
2846
  if (!node.selfClosing && !node.unclosed) {
2847
+ // closingElement.name is a separate JSXIdentifier (not the same object as node.id)
2848
+ // so we need to capitalize it separately
2849
+ if (node.closingElement.name && 'name' in node.closingElement.name) {
2850
+ /** @type {{ name: string }} */ (node.closingElement.name).name = capitalized_name;
2851
+ }
2814
2852
  node.closingElement.metadata = {
2815
2853
  ...node.closingElement.metadata,
2816
2854
  is_capitalized: true,
@@ -3064,6 +3102,7 @@ function transform_ts_child(node, context) {
3064
3102
  function is_template_or_control_flow(node) {
3065
3103
  return (
3066
3104
  node.type === 'Element' ||
3105
+ node.type === 'RippleExpression' ||
3067
3106
  node.type === 'Text' ||
3068
3107
  node.type === 'Html' ||
3069
3108
  node.type === 'TsxCompat' ||
@@ -3162,7 +3201,10 @@ function element_has_dynamic_content(element) {
3162
3201
  ) {
3163
3202
  return true;
3164
3203
  }
3165
- if (child.type === 'Text' && child.expression.type !== 'Literal') {
3204
+ if (
3205
+ (child.type === 'RippleExpression' || child.type === 'Text') &&
3206
+ child.expression.type !== 'Literal'
3207
+ ) {
3166
3208
  return true;
3167
3209
  }
3168
3210
  // Non-DOM element (component)
@@ -3334,6 +3376,14 @@ function transform_children(children, context) {
3334
3376
  (node.type === 'Element' &&
3335
3377
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
3336
3378
  ) ||
3379
+ (normalized.filter(
3380
+ (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3381
+ ).length === 1 &&
3382
+ normalized.some(
3383
+ (node) =>
3384
+ node.type === 'RippleExpression' &&
3385
+ is_children_template_expression(node.expression, state.scope),
3386
+ )) ||
3337
3387
  normalized.filter(
3338
3388
  (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3339
3389
  ).length > 1;
@@ -3348,9 +3398,11 @@ function transform_children(children, context) {
3348
3398
  return b.id(
3349
3399
  node.type == 'Element' && is_element_dom_element(node)
3350
3400
  ? state.scope.generate(/** @type {AST.Identifier} */ (node.id).name)
3351
- : node.type == 'Text'
3352
- ? state.scope.generate('text')
3353
- : state.scope.generate('node'),
3401
+ : node.type == 'RippleExpression'
3402
+ ? state.scope.generate('expression')
3403
+ : node.type == 'Text'
3404
+ ? state.scope.generate('text')
3405
+ : state.scope.generate('node'),
3354
3406
  /** @type {AST.NodeWithLocation} */ (node.type === 'Element' ? node.openingElement : node),
3355
3407
  );
3356
3408
  };
@@ -3504,11 +3556,11 @@ function transform_children(children, context) {
3504
3556
  /** @type {AST.Expression | undefined} */
3505
3557
  let expression = undefined;
3506
3558
  let is_create_text_only = false;
3507
- if (node.type === 'Text' || node.type === 'Html') {
3559
+ if (node.type === 'RippleExpression' || node.type === 'Text' || node.type === 'Html') {
3508
3560
  metadata = { tracking: false, await: false };
3509
3561
  expression = /** @type {AST.Expression} */ (visit(node.expression, { ...state, metadata }));
3510
3562
  is_create_text_only =
3511
- node.type === 'Text' && normalized.length === 1 && expression.type === 'Literal';
3563
+ node.type !== 'Html' && normalized.length === 1 && expression.type === 'Literal';
3512
3564
  }
3513
3565
 
3514
3566
  if (initial === null && root && !is_create_text_only) {
@@ -3604,7 +3656,8 @@ function transform_children(children, context) {
3604
3656
  child.type === 'Html' ||
3605
3657
  (child.type === 'Element' &&
3606
3658
  (child.id.type !== 'Identifier' || !is_element_dom_element(child))) ||
3607
- (child.type === 'Text' && child.expression.type !== 'Literal'),
3659
+ ((child.type === 'RippleExpression' || child.type === 'Text') &&
3660
+ child.expression.type !== 'Literal'),
3608
3661
  );
3609
3662
 
3610
3663
  // Add pop() if we have DOM element children AND the Element visitor didn't already add pop()
@@ -3620,7 +3673,7 @@ function transform_children(children, context) {
3620
3673
  // Components always generate sibling()
3621
3674
  needs_sibling_call = true;
3622
3675
  }
3623
- } else if (next_node.type === 'Text') {
3676
+ } else if (next_node.type === 'RippleExpression' || next_node.type === 'Text') {
3624
3677
  // Only dynamic text generates sibling()
3625
3678
  needs_sibling_call = next_node.expression.type !== 'Literal';
3626
3679
  } else if (
@@ -3666,6 +3719,85 @@ function transform_children(children, context) {
3666
3719
  ),
3667
3720
  ),
3668
3721
  });
3722
+ } else if (node.type === 'RippleExpression') {
3723
+ const expr = /** @type {AST.Expression} */ (expression);
3724
+ const is_children_expression = is_children_template_expression(
3725
+ node.expression,
3726
+ state.scope,
3727
+ );
3728
+
3729
+ if (expr.type === 'Literal') {
3730
+ if (normalized.length === 1) {
3731
+ skipped++;
3732
+ if (
3733
+ /** @type {NonNullable<TransformClientState['template']>} */ (state.template).length >
3734
+ 0
3735
+ ) {
3736
+ state.template?.push(escape_html(expr.value));
3737
+ } else {
3738
+ const id = flush_node(true);
3739
+ state.init?.push(b.var(/** @type {AST.Identifier} */ (id), b.call('_$_.text', expr)));
3740
+ state.final?.push(b.stmt(b.call('_$_.append', b.id('__anchor'), id)));
3741
+ }
3742
+ } else {
3743
+ skipped++;
3744
+ state.template?.push(escape_html(expr.value));
3745
+ }
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) {
3775
+ skipped++;
3776
+ const id = flush_node(true);
3777
+ state.template?.push(' ');
3778
+ state.init?.push(
3779
+ b.stmt(
3780
+ b.assignment(
3781
+ '=',
3782
+ b.member(/** @type {AST.Identifier} */ (id), b.id('nodeValue')),
3783
+ expr,
3784
+ ),
3785
+ ),
3786
+ );
3787
+ } 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
+ }
3800
+ }
3669
3801
  } else if (node.type === 'Text') {
3670
3802
  if (metadata?.tracking) {
3671
3803
  skipped = 0;
@@ -23,6 +23,7 @@ import {
23
23
  is_inside_component,
24
24
  is_void_element,
25
25
  normalize_children,
26
+ is_children_template_expression,
26
27
  is_binding_function,
27
28
  is_element_dynamic,
28
29
  is_ripple_track_call,
@@ -51,6 +52,7 @@ import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../constants.js';
51
52
  function is_template_or_control_flow(node) {
52
53
  return (
53
54
  node.type === 'Element' ||
55
+ node.type === 'RippleExpression' ||
54
56
  node.type === 'Text' ||
55
57
  node.type === 'Html' ||
56
58
  node.type === 'TsxCompat' ||
@@ -1082,7 +1084,7 @@ const visitors = {
1082
1084
  } else {
1083
1085
  /** @type {(AST.Property | AST.SpreadElement)[]} */
1084
1086
  const props = [];
1085
- /** @type {AST.Expression | null} */
1087
+ /** @type {AST.Property | null} */
1086
1088
  let children_prop = null;
1087
1089
 
1088
1090
  const apply_parent_css_scope = state.applyParentCssScope;
@@ -1102,7 +1104,11 @@ const visitors = {
1102
1104
  );
1103
1105
 
1104
1106
  if (attr.name.name === 'children') {
1105
- children_prop = attr.name.tracked ? b.thunk(property) : property;
1107
+ children_prop = b.prop(
1108
+ 'init',
1109
+ b.id('children'),
1110
+ b.call('_$_.normalize_children', property),
1111
+ );
1106
1112
  continue;
1107
1113
  }
1108
1114
 
@@ -1119,50 +1125,44 @@ const visitors = {
1119
1125
  }
1120
1126
  }
1121
1127
 
1122
- const children_filtered = [];
1128
+ const children_filtered = node.children.filter(
1129
+ (child) => child.type !== 'EmptyStatement' && child.type !== 'Component',
1130
+ );
1123
1131
 
1124
- for (const child of node.children) {
1125
- if (child.type === 'Component') {
1126
- // in this case, id cannot be null
1127
- // as these are direct children of the component
1128
- const id = /** @type {AST.Identifier} */ (child.id);
1129
- props.push(
1130
- b.prop(
1131
- 'init',
1132
- id,
1133
- /** @type {AST.Expression} */ (
1134
- visit(child, { ...state, namespace: child_namespace })
1135
- ),
1136
- ),
1132
+ if (children_filtered.length > 0) {
1133
+ const component_scope = /** @type {ScopeInterface} */ (context.state.scopes.get(node));
1134
+ const children = b.call(
1135
+ '_$_.ripple_element',
1136
+ /** @type {AST.Expression} */ (
1137
+ visit(b.component(b.id('render_children'), [], children_filtered), {
1138
+ ...context.state,
1139
+ ...(apply_parent_css_scope ||
1140
+ (is_element_dynamic(node) && node.metadata.scoped && state.component?.css)
1141
+ ? {
1142
+ applyParentCssScope:
1143
+ apply_parent_css_scope ||
1144
+ /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1145
+ }
1146
+ : {}),
1147
+ scope: component_scope,
1148
+ namespace: child_namespace,
1149
+ })
1150
+ ),
1151
+ );
1152
+
1153
+ if (children_prop) {
1154
+ children_prop.value = b.logical(
1155
+ '??',
1156
+ /** @type {AST.Expression} */ (children_prop.value),
1157
+ children,
1137
1158
  );
1138
1159
  } else {
1139
- children_filtered.push(child);
1160
+ children_prop = b.prop('init', b.id('children'), children);
1140
1161
  }
1141
1162
  }
1142
1163
 
1143
1164
  if (children_prop) {
1144
- props.push(b.prop('init', b.id('children'), children_prop));
1145
- }
1146
-
1147
- if (children_filtered.length > 0) {
1148
- const component_scope = /** @type {ScopeInterface} */ (context.state.scopes.get(node));
1149
- const children = /** @type {AST.Expression} */ (
1150
- visit(b.component(b.id('children'), [], children_filtered), {
1151
- ...context.state,
1152
- ...(apply_parent_css_scope ||
1153
- (is_element_dynamic(node) && node.metadata.scoped && state.component?.css)
1154
- ? {
1155
- applyParentCssScope:
1156
- apply_parent_css_scope ||
1157
- /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1158
- }
1159
- : {}),
1160
- scope: component_scope,
1161
- namespace: child_namespace,
1162
- })
1163
- );
1164
-
1165
- props.push(b.prop('init', b.id('children'), children));
1165
+ props.push(children_prop);
1166
1166
  }
1167
1167
 
1168
1168
  // For SSR, determine if we should await based on component metadata
@@ -1615,9 +1615,10 @@ const visitors = {
1615
1615
  return context.next();
1616
1616
  },
1617
1617
 
1618
- Text(node, { visit, state }) {
1618
+ RippleExpression(node, { visit, state }) {
1619
1619
  const metadata = { await: false };
1620
1620
  let expression = /** @type {AST.Expression} */ (visit(node.expression, { ...state, metadata }));
1621
+ const is_children_expression = is_children_template_expression(node.expression, state.scope);
1621
1622
 
1622
1623
  if (expression.type === 'Literal') {
1623
1624
  state.init?.push(
@@ -1625,6 +1626,8 @@ const visitors = {
1625
1626
  b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
1626
1627
  ),
1627
1628
  );
1629
+ } else if (is_children_expression) {
1630
+ state.init?.push(b.stmt(b.call('_$_.render_expression', b.id('__output'), expression)));
1628
1631
  } else {
1629
1632
  state.init?.push(
1630
1633
  b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.call('_$_.escape', expression))),
@@ -1632,6 +1635,25 @@ const visitors = {
1632
1635
  }
1633
1636
  },
1634
1637
 
1638
+ Text(node, context) {
1639
+ const metadata = { await: false };
1640
+ let expression = /** @type {AST.Expression} */ (
1641
+ context.visit(node.expression, { ...context.state, metadata })
1642
+ );
1643
+
1644
+ if (expression.type === 'Literal') {
1645
+ context.state.init?.push(
1646
+ b.stmt(
1647
+ b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
1648
+ ),
1649
+ );
1650
+ } else {
1651
+ context.state.init?.push(
1652
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.call('_$_.escape', expression))),
1653
+ );
1654
+ }
1655
+ },
1656
+
1635
1657
  Html(node, { visit, state }) {
1636
1658
  const metadata = { await: false };
1637
1659
  const expression = /** @type {AST.Expression} */ (
@@ -134,6 +134,7 @@ declare module 'estree' {
134
134
  interface NodeMap {
135
135
  Component: Component;
136
136
  TsxCompat: TsxCompat;
137
+ RippleExpression: RippleExpression;
137
138
  Html: Html;
138
139
  Element: Element;
139
140
  Text: TextNode;
@@ -282,7 +283,13 @@ declare module 'estree' {
282
283
 
283
284
  interface Html extends AST.BaseNode {
284
285
  type: 'Html';
285
- expression: Expression;
286
+ expression: AST.Expression;
287
+ }
288
+
289
+ export interface RippleExpression extends AST.BaseExpression {
290
+ type: 'RippleExpression';
291
+ expression: AST.Expression;
292
+ loc?: AST.SourceLocation;
286
293
  }
287
294
 
288
295
  interface Element extends AST.BaseNode {
@@ -601,6 +608,7 @@ declare module 'estree-jsx' {
601
608
 
602
609
  interface JSXExpressionContainer {
603
610
  html?: boolean;
611
+ text?: boolean;
604
612
  }
605
613
 
606
614
  interface JSXMemberExpression {
@@ -1195,6 +1195,8 @@ export namespace Parse {
1195
1195
  topLevel?: boolean,
1196
1196
  exports?: AST.ExportSpecifier,
1197
1197
  ):
1198
+ | AST.RippleExpression
1199
+ | AST.Html
1198
1200
  | AST.TextNode
1199
1201
  | ESTreeJSX.JSXEmptyExpression
1200
1202
  | ESTreeJSX.JSXExpressionContainer
@@ -631,7 +631,22 @@ export function normalize_children(children, context) {
631
631
  const child = normalized[i];
632
632
  const prev_child = normalized[i - 1];
633
633
 
634
- if (child.type === 'Text' && prev_child?.type === 'Text') {
634
+ if (
635
+ (child.type === 'RippleExpression' || child.type === 'Text') &&
636
+ (prev_child?.type === 'RippleExpression' || prev_child?.type === 'Text')
637
+ ) {
638
+ if (
639
+ (child.type === 'RippleExpression' &&
640
+ is_children_template_expression(child.expression, context.state.scope)) ||
641
+ (prev_child.type === 'RippleExpression' &&
642
+ is_children_template_expression(prev_child.expression, context.state.scope))
643
+ ) {
644
+ continue;
645
+ }
646
+
647
+ if (prev_child.type === 'Text' || child.type === 'Text') {
648
+ prev_child.type = 'Text';
649
+ }
635
650
  if (child.expression.type === 'Literal' && prev_child.expression.type === 'Literal') {
636
651
  prev_child.expression = b.literal(
637
652
  prev_child.expression.value + String(child.expression.value),
@@ -650,6 +665,91 @@ export function normalize_children(children, context) {
650
665
  return normalized;
651
666
  }
652
667
 
668
+ /**
669
+ * @param {AST.Expression} expression
670
+ * @returns {AST.Expression}
671
+ */
672
+ export function unwrap_template_expression(expression) {
673
+ /** @type {AST.Expression} */
674
+ let node = expression;
675
+
676
+ while (true) {
677
+ if (
678
+ node.type === 'ParenthesizedExpression' ||
679
+ node.type === 'TSAsExpression' ||
680
+ node.type === 'TSSatisfiesExpression' ||
681
+ node.type === 'TSNonNullExpression' ||
682
+ node.type === 'TSInstantiationExpression'
683
+ ) {
684
+ node = /** @type {AST.Expression} */ (node.expression);
685
+ continue;
686
+ }
687
+
688
+ if (node.type === 'ChainExpression') {
689
+ node = /** @type {AST.Expression} */ (node.expression);
690
+ continue;
691
+ }
692
+
693
+ break;
694
+ }
695
+
696
+ return node;
697
+ }
698
+
699
+ /**
700
+ * @param {AST.Expression} expression
701
+ * @param {ScopeInterface | null | undefined} scope
702
+ * @param {ScopeInterface | null} [component_scope]
703
+ * @returns {boolean}
704
+ */
705
+ export function is_children_template_expression(expression, scope, component_scope = null) {
706
+ if (scope == null) {
707
+ return false;
708
+ }
709
+
710
+ const unwrapped = unwrap_template_expression(expression);
711
+
712
+ if (unwrapped.type === 'MemberExpression') {
713
+ let property_name = null;
714
+
715
+ if (!unwrapped.computed && unwrapped.property.type === 'Identifier') {
716
+ property_name = unwrapped.property.name;
717
+ } else if (
718
+ unwrapped.computed &&
719
+ unwrapped.property.type === 'Literal' &&
720
+ typeof unwrapped.property.value === 'string'
721
+ ) {
722
+ property_name = unwrapped.property.value;
723
+ }
724
+
725
+ if (property_name === 'children') {
726
+ const target = unwrap_template_expression(/** @type {AST.Expression} */ (unwrapped.object));
727
+
728
+ if (target.type === 'Identifier') {
729
+ const binding = scope.get(target.name);
730
+ return (
731
+ binding?.declaration_kind === 'param' &&
732
+ (component_scope === null || binding.scope === component_scope)
733
+ );
734
+ }
735
+ }
736
+ }
737
+
738
+ if (unwrapped.type !== 'Identifier' || unwrapped.name !== 'children') {
739
+ return false;
740
+ }
741
+
742
+ const binding = scope.get(unwrapped.name);
743
+ return (
744
+ (binding?.declaration_kind === 'param' ||
745
+ binding?.kind === 'prop' ||
746
+ binding?.kind === 'prop_fallback' ||
747
+ binding?.kind === 'lazy' ||
748
+ binding?.kind === 'lazy_fallback') &&
749
+ (component_scope === null || binding.scope === component_scope)
750
+ );
751
+ }
752
+
653
753
  /**
654
754
  * @param {AST.Node} node
655
755
  * @param {AST.Node[]} normalized