ripple 0.3.9 → 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 (70) hide show
  1. package/CHANGELOG.md +43 -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 +195 -23
  6. package/src/compiler/phases/2-analyze/index.js +266 -108
  7. package/src/compiler/phases/2-analyze/prune.js +13 -5
  8. package/src/compiler/phases/3-transform/client/index.js +304 -80
  9. package/src/compiler/phases/3-transform/server/index.js +108 -43
  10. package/src/compiler/types/index.d.ts +28 -3
  11. package/src/compiler/types/parse.d.ts +3 -1
  12. package/src/compiler/utils.js +275 -1
  13. package/src/runtime/element.js +39 -0
  14. package/src/runtime/index-client.js +14 -4
  15. package/src/runtime/internal/client/composite.js +10 -6
  16. package/src/runtime/internal/client/expression.js +280 -0
  17. package/src/runtime/internal/client/index.js +4 -0
  18. package/src/runtime/internal/client/portal.js +12 -6
  19. package/src/runtime/internal/server/index.js +26 -1
  20. package/src/utils/builders.js +30 -0
  21. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  22. package/tests/client/basic/basic.components.test.ripple +85 -87
  23. package/tests/client/basic/basic.errors.test.ripple +4 -8
  24. package/tests/client/basic/basic.rendering.test.ripple +27 -10
  25. package/tests/client/capture-error.js +12 -0
  26. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  27. package/tests/client/composite/composite.props.test.ripple +1 -3
  28. package/tests/client/composite/composite.render.test.ripple +91 -13
  29. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  30. package/tests/client/return.test.ripple +101 -0
  31. package/tests/client/svg.test.ripple +4 -4
  32. package/tests/client/tsx.test.ripple +486 -0
  33. package/tests/hydration/basic.test.js +23 -0
  34. package/tests/hydration/compiled/client/basic.js +111 -75
  35. package/tests/hydration/compiled/client/composite.js +81 -46
  36. package/tests/hydration/compiled/client/events.js +18 -63
  37. package/tests/hydration/compiled/client/for.js +90 -183
  38. package/tests/hydration/compiled/client/head.js +10 -25
  39. package/tests/hydration/compiled/client/hmr.js +10 -13
  40. package/tests/hydration/compiled/client/html.js +251 -380
  41. package/tests/hydration/compiled/client/if-children.js +35 -45
  42. package/tests/hydration/compiled/client/if.js +2 -2
  43. package/tests/hydration/compiled/client/mixed-control-flow.js +24 -72
  44. package/tests/hydration/compiled/client/nested-control-flow.js +115 -391
  45. package/tests/hydration/compiled/client/portal.js +8 -20
  46. package/tests/hydration/compiled/client/reactivity.js +14 -47
  47. package/tests/hydration/compiled/client/return.js +2 -5
  48. package/tests/hydration/compiled/client/try.js +4 -4
  49. package/tests/hydration/compiled/server/basic.js +64 -31
  50. package/tests/hydration/compiled/server/composite.js +62 -29
  51. package/tests/hydration/compiled/server/hmr.js +24 -37
  52. package/tests/hydration/compiled/server/html.js +472 -611
  53. package/tests/hydration/compiled/server/if-children.js +77 -103
  54. package/tests/hydration/compiled/server/portal.js +8 -8
  55. package/tests/hydration/components/basic.ripple +15 -5
  56. package/tests/hydration/components/composite.ripple +13 -1
  57. package/tests/hydration/components/hmr.ripple +1 -3
  58. package/tests/hydration/components/html.ripple +13 -35
  59. package/tests/hydration/components/if-children.ripple +4 -8
  60. package/tests/hydration/composite.test.js +11 -0
  61. package/tests/server/basic.attributes.test.ripple +50 -0
  62. package/tests/server/basic.components.test.ripple +22 -28
  63. package/tests/server/basic.test.ripple +12 -0
  64. package/tests/server/compiler.test.ripple +25 -8
  65. package/tests/server/composite.props.test.ripple +1 -3
  66. package/tests/server/style-identifier.test.ripple +2 -4
  67. package/tests/utils/compiler-compat-config.test.js +38 -0
  68. package/tests/utils/vite-plugin-config.test.js +113 -0
  69. package/tsconfig.typecheck.json +2 -1
  70. package/types/index.d.ts +8 -11
@@ -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,
@@ -60,6 +61,7 @@ import {
60
61
  is_ripple_import,
61
62
  replace_lazy_param_pattern,
62
63
  ripple_import_requires_block,
64
+ jsx_to_ripple_node,
63
65
  } from '../../../utils.js';
64
66
  import {
65
67
  CSS_HASH_IDENTIFIER,
@@ -857,6 +859,8 @@ const visitors = {
857
859
  // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
858
860
  if (
859
861
  node.expression.type === 'AssignmentExpression' &&
862
+ (node.expression.left.type === 'ObjectPattern' ||
863
+ node.expression.left.type === 'ArrayPattern') &&
860
864
  node.expression.left.lazy &&
861
865
  node.expression.left.metadata?.lazy_id
862
866
  ) {
@@ -1139,6 +1143,16 @@ const visitors = {
1139
1143
  TsxCompat(node, context) {
1140
1144
  const { state, visit } = context;
1141
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
+
1142
1156
  state.template?.push('<!>');
1143
1157
 
1144
1158
  const normalized_children = node.children.filter((child) => {
@@ -1176,6 +1190,58 @@ const visitors = {
1176
1190
  );
1177
1191
  },
1178
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
+
1179
1245
  Element(node, context) {
1180
1246
  const { state, visit } = context;
1181
1247
 
@@ -1573,11 +1639,13 @@ const visitors = {
1573
1639
  child.type === 'TryStatement' ||
1574
1640
  child.type === 'ForOfStatement' ||
1575
1641
  child.type === 'SwitchStatement' ||
1642
+ child.type === 'Tsx' ||
1576
1643
  child.type === 'TsxCompat' ||
1577
1644
  child.type === 'Html' ||
1578
1645
  (child.type === 'Element' &&
1579
1646
  (child.id.type !== 'Identifier' || !is_element_dom_element(child))) ||
1580
- (child.type === 'Text' && child.expression.type !== 'Literal'),
1647
+ ((child.type === 'RippleExpression' || child.type === 'Text') &&
1648
+ child.expression.type !== 'Literal'),
1581
1649
  );
1582
1650
 
1583
1651
  if (needs_pop) {
@@ -1611,7 +1679,7 @@ const visitors = {
1611
1679
  const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
1612
1680
  /** @type {(AST.Property | AST.SpreadElement)[]} */
1613
1681
  const props = [];
1614
- /** @type {AST.Expression | AST.BlockStatement | null} */
1682
+ /** @type {AST.Property | null} */
1615
1683
  let children_prop = null;
1616
1684
 
1617
1685
  for (const attr of node.attributes) {
@@ -1621,7 +1689,9 @@ const visitors = {
1621
1689
  let property =
1622
1690
  attr.value === null
1623
1691
  ? b.literal(true)
1624
- : /** @type {AST.Expression} */ (visit(attr.value, { ...state, metadata }));
1692
+ : /** @type {AST.Expression} */ (
1693
+ visit(attr.value, { ...state, flush_node: null, metadata })
1694
+ );
1625
1695
 
1626
1696
  if (attr.name.name === 'class' && node.metadata.scoped && state.component?.css) {
1627
1697
  if (property.type === 'Literal') {
@@ -1633,7 +1703,15 @@ const visitors = {
1633
1703
 
1634
1704
  if (metadata.tracking || attr.name.tracked) {
1635
1705
  if (attr.name.name === 'children') {
1636
- children_prop = b.thunk(property);
1706
+ children_prop = b.prop(
1707
+ 'get',
1708
+ b.id('children'),
1709
+ b.function(
1710
+ null,
1711
+ [],
1712
+ b.block([b.return(b.call('_$_.normalize_children', property))]),
1713
+ ),
1714
+ );
1637
1715
  continue;
1638
1716
  }
1639
1717
 
@@ -1645,6 +1723,15 @@ const visitors = {
1645
1723
  ),
1646
1724
  );
1647
1725
  } else {
1726
+ if (attr.name.name === 'children') {
1727
+ children_prop = b.prop(
1728
+ 'init',
1729
+ b.id('children'),
1730
+ b.call('_$_.normalize_children', property),
1731
+ );
1732
+ continue;
1733
+ }
1734
+
1648
1735
  props.push(b.prop('init', b.key(attr.name.name), property));
1649
1736
  }
1650
1737
  } else {
@@ -1652,7 +1739,9 @@ const visitors = {
1652
1739
  b.prop(
1653
1740
  'init',
1654
1741
  b.key(attr.name.name),
1655
- /** @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
+ ),
1656
1745
  ),
1657
1746
  );
1658
1747
  }
@@ -1660,7 +1749,13 @@ const visitors = {
1660
1749
  props.push(
1661
1750
  b.spread(
1662
1751
  /** @type {AST.Expression} */
1663
- (visit(attr.argument, { ...state, metadata: { ...state.metadata } })),
1752
+ (
1753
+ visit(attr.argument, {
1754
+ ...state,
1755
+ flush_node: null,
1756
+ metadata: { ...state.metadata },
1757
+ })
1758
+ ),
1664
1759
  ),
1665
1760
  );
1666
1761
  } else if (attr.type === 'RefAttribute') {
@@ -1671,7 +1766,9 @@ const visitors = {
1671
1766
  b.prop(
1672
1767
  'init',
1673
1768
  b.id(ref_id),
1674
- /** @type {AST.Expression} */ (visit(attr.argument, { ...state, metadata })),
1769
+ /** @type {AST.Expression} */ (
1770
+ visit(attr.argument, { ...state, flush_node: null, metadata })
1771
+ ),
1675
1772
  true,
1676
1773
  ),
1677
1774
  );
@@ -1694,60 +1791,63 @@ const visitors = {
1694
1791
  }
1695
1792
  }
1696
1793
 
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
- }
1794
+ const children_filtered = node.children.filter(
1795
+ (child) => child.type !== 'EmptyStatement' && child.type !== 'Component',
1796
+ );
1717
1797
 
1718
1798
  if (children_filtered.length > 0) {
1719
1799
  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
- })
1800
+ const children_component = b.component(b.id('render_children'), [], children_filtered);
1801
+
1802
+ const children = b.call(
1803
+ '_$_.ripple_element',
1804
+ /** @type {AST.Expression} */ (
1805
+ visit(children_component, {
1806
+ ...state,
1807
+ ...(apply_parent_css_scope ||
1808
+ (is_dynamic_element && node.metadata.scoped && state.component?.css)
1809
+ ? {
1810
+ applyParentCssScope:
1811
+ apply_parent_css_scope ||
1812
+ /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1813
+ }
1814
+ : {}),
1815
+ scope: /** @type {ScopeInterface} */ (component_scope),
1816
+ namespace: child_namespace,
1817
+ is_ripple_element: true,
1818
+ })
1819
+ ),
1736
1820
  );
1737
1821
 
1738
1822
  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
- );
1823
+ if (children_prop.kind === 'get') {
1824
+ /** @type {AST.ReturnStatement} */ (
1825
+ /** @type {AST.FunctionExpression} */ (children_prop.value).body.body[0]
1826
+ ).argument = b.logical(
1827
+ '??',
1828
+ /** @type {AST.Expression} */ (
1829
+ /** @type {AST.ReturnStatement} */ (
1830
+ /** @type {AST.FunctionExpression} */ (children_prop.value).body.body[0]
1831
+ ).argument
1832
+ ),
1833
+ children,
1834
+ );
1835
+ } else {
1836
+ children_prop.value = b.logical(
1837
+ '??',
1838
+ /** @type {AST.Expression} */ (children_prop.value),
1839
+ children,
1840
+ );
1841
+ }
1746
1842
  } else {
1747
- props.push(b.prop('init', b.id('children'), children));
1843
+ children_prop = b.prop('init', b.id('children'), children);
1748
1844
  }
1749
1845
  }
1750
1846
 
1847
+ if (children_prop) {
1848
+ props.push(children_prop);
1849
+ }
1850
+
1751
1851
  const metadata = { tracking: false, await: false };
1752
1852
  // We visit, but only to gather metadata
1753
1853
  b.call(/** @type {AST.Expression} */ (visit(node.id, { ...state, metadata })));
@@ -1932,30 +2032,46 @@ const visitors = {
1932
2032
  }
1933
2033
 
1934
2034
  const component_scope = context.state.scopes.get(node) || context.state.scope;
1935
- const body_statements = [
1936
- b.stmt(b.call('_$_.push_component')),
1937
- ...transform_body(node.body, {
1938
- ...context,
1939
- state: {
1940
- ...context.state,
1941
- flush_node: null,
1942
- component: node,
1943
- metadata,
1944
- scope: component_scope,
1945
- },
1946
- }),
1947
- b.stmt(b.call('_$_.pop_component')),
1948
- ];
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
+ ];
1949
2059
 
1950
2060
  if (node.css !== null && node.css) {
1951
2061
  context.state.stylesheets.push(node.css);
1952
2062
  }
1953
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
+
1954
2072
  const func = b.function(
1955
2073
  node.id,
1956
- node.params.length > 0
1957
- ? [b.id('__anchor'), props, b.id('__block')]
1958
- : [b.id('__anchor'), b.id('_'), b.id('__block')],
2074
+ params,
1959
2075
  b.block([
1960
2076
  ...style_statements,
1961
2077
  ...(prop_statements ?? []),
@@ -2638,7 +2754,7 @@ function join_template(items) {
2638
2754
  function transform_ts_child(node, context) {
2639
2755
  const { state, visit } = context;
2640
2756
 
2641
- if (node.type === 'Text') {
2757
+ if (node.type === 'RippleExpression' || node.type === 'Text') {
2642
2758
  state.init?.push(b.stmt(/** @type {AST.Expression} */ (visit(node.expression, { ...state }))));
2643
2759
  } else if (node.type === 'Html') {
2644
2760
  // Do we need to do something special here?
@@ -2800,17 +2916,37 @@ function transform_ts_child(node, context) {
2800
2916
  }
2801
2917
  }
2802
2918
 
2803
- 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
+ ) {
2804
2923
  // This is just temporary until we remove capitalization
2805
2924
  // The `is_capitalized` was never handled for MemberExpression
2806
2925
  // but it should've been for the `object` part because it starts the tag
2807
2926
  // But the plan is to only rely on source_name and creating a const for the tag with ['#v']
2927
+ const source_name = /** @type {AST.Identifier} */ (node.id).name;
2928
+ const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
2929
+
2930
+ // node.id and node.openingElement.name are the SAME object (convert_from_jsx mutates
2931
+ // the JSXIdentifier to an Identifier in-place). Capitalize the name directly so that
2932
+ // the generated JSX uses <Tag> (uppercase) matching the capitalized variable declaration,
2933
+ // preventing the TypeScript "declared but never read" false-negative (ts6133).
2934
+ /** @type {AST.Identifier} */ (node.id).name = capitalized_name;
2935
+ if (!node.id.metadata) node.id.metadata = /** @type {any} */ ({});
2936
+ node.id.metadata.is_capitalized = true;
2937
+ node.id.metadata.source_name = source_name;
2938
+
2808
2939
  node.openingElement.metadata = {
2809
2940
  ...node.openingElement.metadata,
2810
2941
  is_capitalized: true,
2811
2942
  };
2812
2943
 
2813
2944
  if (!node.selfClosing && !node.unclosed) {
2945
+ // closingElement.name is a separate JSXIdentifier (not the same object as node.id)
2946
+ // so we need to capitalize it separately
2947
+ if (node.closingElement.name && 'name' in node.closingElement.name) {
2948
+ /** @type {{ name: string }} */ (node.closingElement.name).name = capitalized_name;
2949
+ }
2814
2950
  node.closingElement.metadata = {
2815
2951
  ...node.closingElement.metadata,
2816
2952
  is_capitalized: true,
@@ -3026,6 +3162,18 @@ function transform_ts_child(node, context) {
3026
3162
  );
3027
3163
 
3028
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));
3029
3177
  } else if (node.type === 'JSXExpressionContainer') {
3030
3178
  // JSX comments {/* ... */} are JSXExpressionContainer with JSXEmptyExpression
3031
3179
  // These should be preserved in the output as-is for prettier to handle
@@ -3064,8 +3212,10 @@ function transform_ts_child(node, context) {
3064
3212
  function is_template_or_control_flow(node) {
3065
3213
  return (
3066
3214
  node.type === 'Element' ||
3215
+ node.type === 'RippleExpression' ||
3067
3216
  node.type === 'Text' ||
3068
3217
  node.type === 'Html' ||
3218
+ node.type === 'Tsx' ||
3069
3219
  node.type === 'TsxCompat' ||
3070
3220
  node.type === 'IfStatement' ||
3071
3221
  node.type === 'ForOfStatement' ||
@@ -3157,12 +3307,16 @@ function element_has_dynamic_content(element) {
3157
3307
  child.type === 'TryStatement' ||
3158
3308
  child.type === 'ForOfStatement' ||
3159
3309
  child.type === 'SwitchStatement' ||
3310
+ child.type === 'Tsx' ||
3160
3311
  child.type === 'TsxCompat' ||
3161
3312
  child.type === 'Html'
3162
3313
  ) {
3163
3314
  return true;
3164
3315
  }
3165
- if (child.type === 'Text' && child.expression.type !== 'Literal') {
3316
+ if (
3317
+ (child.type === 'RippleExpression' || child.type === 'Text') &&
3318
+ child.expression.type !== 'Literal'
3319
+ ) {
3166
3320
  return true;
3167
3321
  }
3168
3322
  // Non-DOM element (component)
@@ -3329,11 +3483,29 @@ function transform_children(children, context) {
3329
3483
  node.type === 'TryStatement' ||
3330
3484
  node.type === 'ForOfStatement' ||
3331
3485
  node.type === 'SwitchStatement' ||
3486
+ node.type === 'Tsx' ||
3332
3487
  node.type === 'TsxCompat' ||
3333
3488
  node.type === 'Html' ||
3334
3489
  (node.type === 'Element' &&
3335
3490
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
3336
3491
  ) ||
3492
+ (normalized.filter(
3493
+ (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3494
+ ).length === 1 &&
3495
+ normalized.some(
3496
+ (node) =>
3497
+ node.type === 'RippleExpression' &&
3498
+ is_children_template_expression(node.expression, state.scope),
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
+ )) ||
3337
3509
  normalized.filter(
3338
3510
  (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3339
3511
  ).length > 1;
@@ -3348,9 +3520,11 @@ function transform_children(children, context) {
3348
3520
  return b.id(
3349
3521
  node.type == 'Element' && is_element_dom_element(node)
3350
3522
  ? state.scope.generate(/** @type {AST.Identifier} */ (node.id).name)
3351
- : node.type == 'Text'
3352
- ? state.scope.generate('text')
3353
- : state.scope.generate('node'),
3523
+ : node.type == 'RippleExpression'
3524
+ ? state.scope.generate('expression')
3525
+ : node.type == 'Text'
3526
+ ? state.scope.generate('text')
3527
+ : state.scope.generate('node'),
3354
3528
  /** @type {AST.NodeWithLocation} */ (node.type === 'Element' ? node.openingElement : node),
3355
3529
  );
3356
3530
  };
@@ -3504,11 +3678,11 @@ function transform_children(children, context) {
3504
3678
  /** @type {AST.Expression | undefined} */
3505
3679
  let expression = undefined;
3506
3680
  let is_create_text_only = false;
3507
- if (node.type === 'Text' || node.type === 'Html') {
3681
+ if (node.type === 'RippleExpression' || node.type === 'Text' || node.type === 'Html') {
3508
3682
  metadata = { tracking: false, await: false };
3509
3683
  expression = /** @type {AST.Expression} */ (visit(node.expression, { ...state, metadata }));
3510
3684
  is_create_text_only =
3511
- node.type === 'Text' && normalized.length === 1 && expression.type === 'Literal';
3685
+ node.type !== 'Html' && normalized.length === 1 && expression.type === 'Literal';
3512
3686
  }
3513
3687
 
3514
3688
  if (initial === null && root && !is_create_text_only) {
@@ -3600,11 +3774,13 @@ function transform_children(children, context) {
3600
3774
  child.type === 'TryStatement' ||
3601
3775
  child.type === 'ForOfStatement' ||
3602
3776
  child.type === 'SwitchStatement' ||
3777
+ child.type === 'Tsx' ||
3603
3778
  child.type === 'TsxCompat' ||
3604
3779
  child.type === 'Html' ||
3605
3780
  (child.type === 'Element' &&
3606
3781
  (child.id.type !== 'Identifier' || !is_element_dom_element(child))) ||
3607
- (child.type === 'Text' && child.expression.type !== 'Literal'),
3782
+ ((child.type === 'RippleExpression' || child.type === 'Text') &&
3783
+ child.expression.type !== 'Literal'),
3608
3784
  );
3609
3785
 
3610
3786
  // Add pop() if we have DOM element children AND the Element visitor didn't already add pop()
@@ -3620,7 +3796,7 @@ function transform_children(children, context) {
3620
3796
  // Components always generate sibling()
3621
3797
  needs_sibling_call = true;
3622
3798
  }
3623
- } else if (next_node.type === 'Text') {
3799
+ } else if (next_node.type === 'RippleExpression' || next_node.type === 'Text') {
3624
3800
  // Only dynamic text generates sibling()
3625
3801
  needs_sibling_call = next_node.expression.type !== 'Literal';
3626
3802
  } else if (
@@ -3629,6 +3805,7 @@ function transform_children(children, context) {
3629
3805
  next_node.type === 'TryStatement' ||
3630
3806
  next_node.type === 'ForOfStatement' ||
3631
3807
  next_node.type === 'SwitchStatement' ||
3808
+ next_node.type === 'Tsx' ||
3632
3809
  next_node.type === 'TsxCompat'
3633
3810
  ) {
3634
3811
  needs_sibling_call = true;
@@ -3640,7 +3817,7 @@ function transform_children(children, context) {
3640
3817
  }
3641
3818
  }
3642
3819
  }
3643
- } else if (node.type === 'TsxCompat') {
3820
+ } else if (node.type === 'TsxCompat' || node.type === 'Tsx') {
3644
3821
  skipped = 0;
3645
3822
 
3646
3823
  visit(node, {
@@ -3666,6 +3843,50 @@ function transform_children(children, context) {
3666
3843
  ),
3667
3844
  ),
3668
3845
  });
3846
+ } else if (node.type === 'RippleExpression') {
3847
+ const expr = /** @type {AST.Expression} */ (expression);
3848
+
3849
+ if (expr.type === 'Literal') {
3850
+ if (normalized.length === 1) {
3851
+ skipped++;
3852
+ if (
3853
+ /** @type {NonNullable<TransformClientState['template']>} */ (state.template).length >
3854
+ 0
3855
+ ) {
3856
+ state.template?.push(escape_html(expr.value));
3857
+ } else {
3858
+ const id = flush_node(true);
3859
+ state.init?.push(b.var(/** @type {AST.Identifier} */ (id), b.call('_$_.text', expr)));
3860
+ state.final?.push(b.stmt(b.call('_$_.append', b.id('__anchor'), id)));
3861
+ }
3862
+ } else {
3863
+ skipped++;
3864
+ state.template?.push(escape_html(expr.value));
3865
+ }
3866
+ } else if (
3867
+ normalized.length === 1 &&
3868
+ !is_children_template_expression(node.expression, state.scope)
3869
+ ) {
3870
+ skipped++;
3871
+ state.template?.push(' ');
3872
+ const id = flush_node(true);
3873
+ const call = b.call('_$_.expression', id, b.thunk(expr));
3874
+ state.init?.push(
3875
+ state.namespace !== DEFAULT_NAMESPACE
3876
+ ? b.stmt(b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(call)))
3877
+ : b.stmt(call),
3878
+ );
3879
+ } else {
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
+ );
3889
+ }
3669
3890
  } else if (node.type === 'Text') {
3670
3891
  if (metadata?.tracking) {
3671
3892
  skipped = 0;
@@ -4267,7 +4488,10 @@ function create_tsx_with_typescript_support(comments) {
4267
4488
  // Shorthand object properties require an Identifier value. When the
4268
4489
  // transformed value is a tracked MemberExpression (for example
4269
4490
  // @value), emit longhand to keep valid output.
4270
- 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
+ ) {
4271
4495
  context.visit(node.key);
4272
4496
  context.write(': ');
4273
4497
  context.visit(node.value);