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
@@ -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,
@@ -31,6 +32,7 @@ import {
31
32
  hash,
32
33
  flatten_switch_consequent,
33
34
  get_ripple_namespace_call_name,
35
+ jsx_to_ripple_node,
34
36
  } from '../../../utils.js';
35
37
  import { escape } from '../../../../utils/escaping.js';
36
38
  import { is_event_attribute } from '../../../../utils/events.js';
@@ -51,8 +53,10 @@ import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../constants.js';
51
53
  function is_template_or_control_flow(node) {
52
54
  return (
53
55
  node.type === 'Element' ||
56
+ node.type === 'RippleExpression' ||
54
57
  node.type === 'Text' ||
55
58
  node.type === 'Html' ||
59
+ node.type === 'Tsx' ||
56
60
  node.type === 'TsxCompat' ||
57
61
  node.type === 'IfStatement' ||
58
62
  node.type === 'ForOfStatement' ||
@@ -197,7 +201,7 @@ function transform_children(children, context) {
197
201
  }
198
202
  }
199
203
  } else {
200
- visit(node, { ...state, return_flags });
204
+ visit(node, { ...state, return_flags, template_child: true });
201
205
  }
202
206
  };
203
207
 
@@ -400,14 +404,22 @@ const visitors = {
400
404
  b.stmt(b.call('_$_.push_component')),
401
405
  ...transform_body(node.body, {
402
406
  ...context,
403
- 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
+ },
404
414
  }),
405
415
  b.stmt(b.call('_$_.pop_component')),
406
416
  );
407
417
 
408
418
  let component_fn = b.function(
409
419
  node.id,
410
- 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')],
411
423
  b.block([
412
424
  ...(metadata.await
413
425
  ? [b.return(b.call('_$_.async', b.thunk(b.block(body_statements), true)))]
@@ -805,6 +817,8 @@ const visitors = {
805
817
  // Handle standalone lazy destructuring: &[data] = track(0); → const lazy0 = track(0);
806
818
  if (
807
819
  node.expression.type === 'AssignmentExpression' &&
820
+ (node.expression.left.type === 'ObjectPattern' ||
821
+ node.expression.left.type === 'ArrayPattern') &&
808
822
  node.expression.left.lazy &&
809
823
  node.expression.left.metadata?.lazy_id
810
824
  ) {
@@ -1082,7 +1096,7 @@ const visitors = {
1082
1096
  } else {
1083
1097
  /** @type {(AST.Property | AST.SpreadElement)[]} */
1084
1098
  const props = [];
1085
- /** @type {AST.Expression | null} */
1099
+ /** @type {AST.Property | null} */
1086
1100
  let children_prop = null;
1087
1101
 
1088
1102
  const apply_parent_css_scope = state.applyParentCssScope;
@@ -1102,7 +1116,11 @@ const visitors = {
1102
1116
  );
1103
1117
 
1104
1118
  if (attr.name.name === 'children') {
1105
- children_prop = attr.name.tracked ? b.thunk(property) : property;
1119
+ children_prop = b.prop(
1120
+ 'init',
1121
+ b.id('children'),
1122
+ b.call('_$_.normalize_children', property),
1123
+ );
1106
1124
  continue;
1107
1125
  }
1108
1126
 
@@ -1119,50 +1137,44 @@ const visitors = {
1119
1137
  }
1120
1138
  }
1121
1139
 
1122
- const children_filtered = [];
1140
+ const children_filtered = node.children.filter(
1141
+ (child) => child.type !== 'EmptyStatement' && child.type !== 'Component',
1142
+ );
1123
1143
 
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
- ),
1144
+ if (children_filtered.length > 0) {
1145
+ const component_scope = /** @type {ScopeInterface} */ (context.state.scopes.get(node));
1146
+ const children = b.call(
1147
+ '_$_.ripple_element',
1148
+ /** @type {AST.Expression} */ (
1149
+ visit(b.component(b.id('render_children'), [], children_filtered), {
1150
+ ...context.state,
1151
+ ...(apply_parent_css_scope ||
1152
+ (is_element_dynamic(node) && node.metadata.scoped && state.component?.css)
1153
+ ? {
1154
+ applyParentCssScope:
1155
+ apply_parent_css_scope ||
1156
+ /** @type {AST.CSS.StyleSheet} */ (state.component?.css).hash,
1157
+ }
1158
+ : {}),
1159
+ scope: component_scope,
1160
+ namespace: child_namespace,
1161
+ })
1162
+ ),
1163
+ );
1164
+
1165
+ if (children_prop) {
1166
+ children_prop.value = b.logical(
1167
+ '??',
1168
+ /** @type {AST.Expression} */ (children_prop.value),
1169
+ children,
1137
1170
  );
1138
1171
  } else {
1139
- children_filtered.push(child);
1172
+ children_prop = b.prop('init', b.id('children'), children);
1140
1173
  }
1141
1174
  }
1142
1175
 
1143
1176
  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));
1177
+ props.push(children_prop);
1166
1178
  }
1167
1179
 
1168
1180
  // For SSR, determine if we should await based on component metadata
@@ -1615,9 +1627,10 @@ const visitors = {
1615
1627
  return context.next();
1616
1628
  },
1617
1629
 
1618
- Text(node, { visit, state }) {
1630
+ RippleExpression(node, { visit, state }) {
1619
1631
  const metadata = { await: false };
1620
1632
  let expression = /** @type {AST.Expression} */ (visit(node.expression, { ...state, metadata }));
1633
+ const is_children_expression = is_children_template_expression(node.expression, state.scope);
1621
1634
 
1622
1635
  if (expression.type === 'Literal') {
1623
1636
  state.init?.push(
@@ -1625,6 +1638,8 @@ const visitors = {
1625
1638
  b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
1626
1639
  ),
1627
1640
  );
1641
+ } else if (is_children_expression) {
1642
+ state.init?.push(b.stmt(b.call('_$_.render_expression', b.id('__output'), expression)));
1628
1643
  } else {
1629
1644
  state.init?.push(
1630
1645
  b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.call('_$_.escape', expression))),
@@ -1632,6 +1647,56 @@ const visitors = {
1632
1647
  }
1633
1648
  },
1634
1649
 
1650
+ Text(node, context) {
1651
+ const metadata = { await: false };
1652
+ let expression = /** @type {AST.Expression} */ (
1653
+ context.visit(node.expression, { ...context.state, metadata })
1654
+ );
1655
+
1656
+ if (expression.type === 'Literal') {
1657
+ context.state.init?.push(
1658
+ b.stmt(
1659
+ b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
1660
+ ),
1661
+ );
1662
+ } else {
1663
+ context.state.init?.push(
1664
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.call('_$_.escape', expression))),
1665
+ );
1666
+ }
1667
+ },
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
+
1635
1700
  Html(node, { visit, state }) {
1636
1701
  const metadata = { await: false };
1637
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,7 +136,9 @@ 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;
141
+ RippleExpression: RippleExpression;
137
142
  Html: Html;
138
143
  Element: Element;
139
144
  Text: TextNode;
@@ -269,6 +274,16 @@ declare module 'estree' {
269
274
  typeParameters?: AST.TSTypeParameterDeclaration;
270
275
  }
271
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
+
272
287
  interface TsxCompat extends AST.BaseNode {
273
288
  type: 'TsxCompat';
274
289
  kind: string;
@@ -282,7 +297,13 @@ declare module 'estree' {
282
297
 
283
298
  interface Html extends AST.BaseNode {
284
299
  type: 'Html';
285
- expression: Expression;
300
+ expression: AST.Expression;
301
+ }
302
+
303
+ export interface RippleExpression extends AST.BaseExpression {
304
+ type: 'RippleExpression';
305
+ expression: AST.Expression;
306
+ loc?: AST.SourceLocation;
286
307
  }
287
308
 
288
309
  interface Element extends AST.BaseNode {
@@ -399,7 +420,7 @@ declare module 'estree' {
399
420
 
400
421
  export type RippleStatement = AST.Statement | TSESTree.Statement;
401
422
 
402
- export type NodeWithChildren = AST.Element | AST.TsxCompat;
423
+ export type NodeWithChildren = AST.Element | AST.Tsx | AST.TsxCompat;
403
424
 
404
425
  export namespace CSS {
405
426
  export interface BaseNode extends AST.NodeWithMaybeComments {
@@ -601,6 +622,7 @@ declare module 'estree-jsx' {
601
622
 
602
623
  interface JSXExpressionContainer {
603
624
  html?: boolean;
625
+ text?: boolean;
604
626
  }
605
627
 
606
628
  interface JSXMemberExpression {
@@ -1262,6 +1284,7 @@ export interface AnalysisState extends BaseState {
1262
1284
  elements?: AST.Element[];
1263
1285
  function_depth?: number;
1264
1286
  loose?: boolean;
1287
+ configured_compat_kinds?: Set<string>;
1265
1288
  metadata: BaseStateMetaData & {
1266
1289
  styleClasses?: StyleClasses;
1267
1290
  };
@@ -1282,6 +1305,7 @@ export interface TransformServerState extends BaseState {
1282
1305
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1283
1306
  dev?: boolean;
1284
1307
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1308
+ template_child?: boolean;
1285
1309
  }
1286
1310
 
1287
1311
  type UpdateList = Array<
@@ -1315,6 +1339,7 @@ export interface TransformClientState extends BaseState {
1315
1339
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1316
1340
  skip_children_traversal: boolean;
1317
1341
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1342
+ is_ripple_element?: boolean;
1318
1343
  }
1319
1344
 
1320
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'])[],
@@ -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
@@ -987,3 +1087,177 @@ export function get_ripple_namespace_call_name(name) {
987
1087
  export function ripple_import_requires_block(name) {
988
1088
  return name !== 'effect' && name !== 'untrack' && name !== 'Context';
989
1089
  }
1090
+
1091
+ /**
1092
+ * Converts a JSXMemberExpression to an AST MemberExpression.
1093
+ * e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
1094
+ * @param {import('estree-jsx').JSXMemberExpression} jsx_member
1095
+ * @returns {AST.MemberExpression}
1096
+ */
1097
+ function jsx_member_expression_to_member_expression(jsx_member) {
1098
+ /** @type {AST.Expression} */
1099
+ let object;
1100
+
1101
+ if (jsx_member.object.type === 'JSXMemberExpression') {
1102
+ // Recursively convert nested member expressions
1103
+ object = jsx_member_expression_to_member_expression(jsx_member.object);
1104
+ } else {
1105
+ // Base case: JSXIdentifier
1106
+ object = /** @type {AST.Identifier} */ ({
1107
+ type: 'Identifier',
1108
+ name: jsx_member.object.name,
1109
+ start: jsx_member.object.start,
1110
+ end: jsx_member.object.end,
1111
+ });
1112
+ }
1113
+
1114
+ return /** @type {AST.MemberExpression} */ ({
1115
+ type: 'MemberExpression',
1116
+ object,
1117
+ property: /** @type {AST.Identifier} */ ({
1118
+ type: 'Identifier',
1119
+ name: jsx_member.property.name,
1120
+ start: jsx_member.property.start,
1121
+ end: jsx_member.property.end,
1122
+ }),
1123
+ computed: false,
1124
+ optional: false,
1125
+ start: jsx_member.start,
1126
+ end: jsx_member.end,
1127
+ });
1128
+ }
1129
+
1130
+ /**
1131
+ * Converts a JSX AST node (JSXElement, JSXText, etc.) to a Ripple AST node
1132
+ * (Element, Text, RippleExpression) for processing inside `<tsx>` blocks.
1133
+ * @param {AST.Node} node
1134
+ * @returns {AST.Node | AST.Node[] | null}
1135
+ */
1136
+ export function jsx_to_ripple_node(node) {
1137
+ if (node.type === 'JSXElement') {
1138
+ const opening = node.openingElement;
1139
+ const name = opening.name;
1140
+
1141
+ /** @type {AST.Identifier | AST.MemberExpression} */
1142
+ let id;
1143
+
1144
+ if (name.type === 'JSXIdentifier') {
1145
+ id = /** @type {AST.Identifier} */ ({
1146
+ type: 'Identifier',
1147
+ name: name.name,
1148
+ start: name.start,
1149
+ end: name.end,
1150
+ });
1151
+ } else if (name.type === 'JSXMemberExpression') {
1152
+ // Convert JSXMemberExpression to MemberExpression
1153
+ // e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
1154
+ id = jsx_member_expression_to_member_expression(name);
1155
+ } else if (name.type === 'JSXNamespacedName') {
1156
+ // For JSXNamespacedName like <namespace:element>, create an identifier with the full name
1157
+ id = /** @type {AST.Identifier} */ ({
1158
+ type: 'Identifier',
1159
+ name: name.namespace.name + ':' + name.name.name,
1160
+ start: name.start,
1161
+ end: name.end,
1162
+ });
1163
+ } else {
1164
+ // Fallback - should not reach here
1165
+ id = /** @type {AST.Identifier} */ ({
1166
+ type: 'Identifier',
1167
+ name: 'unknown',
1168
+ start: /** @type {any} */ (name).start,
1169
+ end: /** @type {any} */ (name).end,
1170
+ });
1171
+ }
1172
+
1173
+ const attributes = opening.attributes
1174
+ .map((attr) => {
1175
+ if (attr.type === 'JSXAttribute') {
1176
+ const is_dynamic = attr.value && attr.value.type === 'JSXExpressionContainer';
1177
+ return /** @type {AST.Node} */ ({
1178
+ type: 'Attribute',
1179
+ name: {
1180
+ type: 'Identifier',
1181
+ name:
1182
+ attr.name.type === 'JSXIdentifier'
1183
+ ? attr.name.name
1184
+ : attr.name.namespace.name + ':' + attr.name.name.name,
1185
+ tracked: is_dynamic,
1186
+ start: attr.name.start,
1187
+ end: attr.name.end,
1188
+ },
1189
+ value: attr.value
1190
+ ? attr.value.type === 'JSXExpressionContainer'
1191
+ ? attr.value.expression
1192
+ : attr.value
1193
+ : null,
1194
+ shorthand: false,
1195
+ start: attr.start,
1196
+ end: attr.end,
1197
+ });
1198
+ } else if (attr.type === 'JSXSpreadAttribute') {
1199
+ return /** @type {AST.Node} */ ({
1200
+ type: 'SpreadAttribute',
1201
+ argument: attr.argument,
1202
+ start: attr.start,
1203
+ end: attr.end,
1204
+ });
1205
+ }
1206
+ return null;
1207
+ })
1208
+ .filter(Boolean);
1209
+
1210
+ const children = /** @type {AST.Node[]} */ (
1211
+ /** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
1212
+ );
1213
+
1214
+ return /** @type {AST.Element} */ (
1215
+ /** @type {unknown} */ ({
1216
+ type: 'Element',
1217
+ id,
1218
+ attributes,
1219
+ children,
1220
+ selfClosing: opening.selfClosing,
1221
+ metadata: { scoped: false, path: /** @type {string[]} */ ([]) },
1222
+ start: node.start,
1223
+ end: node.end,
1224
+ })
1225
+ );
1226
+ }
1227
+
1228
+ if (node.type === 'JSXText') {
1229
+ if (node.value.trim() === '') return null;
1230
+ return /** @type {AST.Node} */ ({
1231
+ type: 'Text',
1232
+ expression: {
1233
+ type: 'Literal',
1234
+ value: node.value,
1235
+ raw: JSON.stringify(node.value),
1236
+ start: node.start,
1237
+ end: node.end,
1238
+ },
1239
+ metadata: {},
1240
+ start: node.start,
1241
+ end: node.end,
1242
+ });
1243
+ }
1244
+
1245
+ if (node.type === 'JSXExpressionContainer') {
1246
+ if (node.expression.type === 'JSXEmptyExpression') return null;
1247
+ return /** @type {AST.Node} */ ({
1248
+ type: 'RippleExpression',
1249
+ expression: node.expression,
1250
+ metadata: {},
1251
+ start: node.start,
1252
+ end: node.end,
1253
+ });
1254
+ }
1255
+
1256
+ if (node.type === 'JSXFragment') {
1257
+ return /** @type {AST.Node[]} */ (
1258
+ /** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
1259
+ );
1260
+ }
1261
+
1262
+ return node;
1263
+ }
@@ -0,0 +1,39 @@
1
+ const RIPPLE_ELEMENT = Symbol.for('ripple.element');
2
+
3
+ /**
4
+ * @typedef {{
5
+ * render: Function;
6
+ * [RIPPLE_ELEMENT]: true;
7
+ * }} RippleElement
8
+ */
9
+
10
+ /**
11
+ * @param {Function} render
12
+ * @returns {RippleElement}
13
+ */
14
+ export function ripple_element(render) {
15
+ return {
16
+ render,
17
+ [RIPPLE_ELEMENT]: true,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * @param {any} value
23
+ * @returns {value is RippleElement}
24
+ */
25
+ export function is_ripple_element(value) {
26
+ return value != null && value[RIPPLE_ELEMENT] === true;
27
+ }
28
+
29
+ /**
30
+ * @param {any} value
31
+ * @returns {any}
32
+ */
33
+ export function normalize_children(value) {
34
+ if (value == null || is_ripple_element(value) || typeof value !== 'function') {
35
+ return value;
36
+ }
37
+
38
+ return ripple_element(value);
39
+ }