ripple 0.2.134 → 0.2.135

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.
@@ -317,21 +317,21 @@ const visitors = {
317
317
  if (!context.state.to_ts) {
318
318
  return b.empty;
319
319
  }
320
- context.next();
320
+ return context.next();
321
321
  },
322
322
 
323
323
  TSInterfaceDeclaration(_, context) {
324
324
  if (!context.state.to_ts) {
325
325
  return b.empty;
326
326
  }
327
- context.next();
327
+ return context.next();
328
328
  },
329
329
 
330
330
  TSMappedType(_, context) {
331
331
  if (!context.state.to_ts) {
332
332
  return b.empty;
333
333
  }
334
- context.next();
334
+ return context.next();
335
335
  },
336
336
 
337
337
  NewExpression(node, context) {
@@ -384,7 +384,7 @@ const visitors = {
384
384
 
385
385
  TrackedArrayExpression(node, context) {
386
386
  if (context.state.to_ts) {
387
- const arrayAlias = import_from_ripple_if_needed("TrackedArray", context);
387
+ const arrayAlias = import_from_ripple_if_needed('TrackedArray', context);
388
388
 
389
389
  return b.call(
390
390
  b.member(b.id(arrayAlias), b.id('from')),
@@ -401,12 +401,9 @@ const visitors = {
401
401
 
402
402
  TrackedObjectExpression(node, context) {
403
403
  if (context.state.to_ts) {
404
- const objectAlias = import_from_ripple_if_needed("TrackedObject", context);
404
+ const objectAlias = import_from_ripple_if_needed('TrackedObject', context);
405
405
 
406
- return b.new(
407
- b.id(objectAlias),
408
- b.object(node.properties.map((prop) => context.visit(prop))),
409
- );
406
+ return b.new(b.id(objectAlias), b.object(node.properties.map((prop) => context.visit(prop))));
410
407
  }
411
408
 
412
409
  return b.call(
@@ -466,14 +463,17 @@ const visitors = {
466
463
  }
467
464
 
468
465
  if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
466
+ // In TypeScript mode, skip the transformation and let transform_ts_child handle it
469
467
  add_ripple_internal_import(context);
470
468
 
471
- return b.call(
472
- '_$_.get_property',
473
- context.visit(node.object),
474
- node.computed ? context.visit(node.property) : b.literal(node.property.name),
475
- node.optional ? b.true : undefined,
476
- );
469
+ if (!context.state.to_ts) {
470
+ return b.call(
471
+ '_$_.get_property',
472
+ context.visit(node.object),
473
+ node.computed ? context.visit(node.property) : b.literal(node.property.name),
474
+ node.optional ? b.true : undefined,
475
+ );
476
+ }
477
477
  }
478
478
 
479
479
  if (node.object.type === 'MemberExpression' && node.object.optional) {
@@ -499,7 +499,7 @@ const visitors = {
499
499
  }
500
500
  }
501
501
  } else {
502
- context.next();
502
+ return context.next();
503
503
  }
504
504
  },
505
505
 
@@ -549,18 +549,68 @@ const visitors = {
549
549
  },
550
550
 
551
551
  JSXText(node, context) {
552
+ if (context.state.to_ts) {
553
+ return context.next();
554
+ }
552
555
  return b.literal(node.value + '');
553
556
  },
554
557
 
555
558
  JSXIdentifier(node, context) {
559
+ if (context.state.to_ts) {
560
+ return context.next();
561
+ }
556
562
  return b.id(node.name);
557
563
  },
558
564
 
559
565
  JSXExpressionContainer(node, context) {
566
+ if (context.state.to_ts) {
567
+ return context.next();
568
+ }
560
569
  return context.visit(node.expression);
561
570
  },
562
571
 
572
+ JSXFragment(node, context) {
573
+ if (context.state.to_ts) {
574
+ return context.next();
575
+ }
576
+ const attributes = node.openingFragment.attributes;
577
+ const normalized_children = node.children.filter((child) => {
578
+ return child.type !== 'JSXText' || child.value.trim() !== '';
579
+ });
580
+
581
+ const props = b.object(
582
+ attributes.map((attr) => {
583
+ if (attr.type === 'JSXAttribute') {
584
+ return b.prop('init', context.visit(attr.name), context.visit(attr.value));
585
+ } else if (attr.type === 'JSXSpreadAttribute') {
586
+ return b.spread(context.visit(attr.argument));
587
+ }
588
+ }),
589
+ );
590
+
591
+ if (normalized_children.length > 0) {
592
+ props.properties.push(
593
+ b.prop(
594
+ 'init',
595
+ b.id('children'),
596
+ normalized_children.length === 1
597
+ ? context.visit(normalized_children[0])
598
+ : b.array(normalized_children.map((child) => context.visit(child))),
599
+ ),
600
+ );
601
+ }
602
+
603
+ return b.call(
604
+ normalized_children.length > 1 ? '__compat.jsxs' : '__compat.jsx',
605
+ b.id('__compat.Fragment'),
606
+ props,
607
+ );
608
+ },
609
+
563
610
  JSXElement(node, context) {
611
+ if (context.state.to_ts) {
612
+ return context.next();
613
+ }
564
614
  const name = node.openingElement.name;
565
615
  const attributes = node.openingElement.attributes;
566
616
  const normalized_children = node.children.filter((child) => {
@@ -577,7 +627,7 @@ const visitors = {
577
627
  }),
578
628
  );
579
629
 
580
- if (normalize_children.length > 0) {
630
+ if (normalized_children.length > 0) {
581
631
  props.properties.push(
582
632
  b.prop(
583
633
  'init',
@@ -590,7 +640,7 @@ const visitors = {
590
640
  }
591
641
 
592
642
  return b.call(
593
- '__compat.jsx',
643
+ normalized_children.length > 1 ? '__compat.jsxs' : '__compat.jsx',
594
644
  name.type === 'JSXIdentifier' && name.name[0].toLowerCase() === name.name[0]
595
645
  ? b.literal(name.name)
596
646
  : context.visit(name),
@@ -682,6 +732,29 @@ const visitors = {
682
732
  const local_updates = [];
683
733
  const is_void = is_void_element(node.id.name);
684
734
 
735
+ let scoping_hash = null;
736
+ if (node.metadata.scoped && state.component.css) {
737
+ scoping_hash = state.component.css.hash;
738
+ } else {
739
+ let inside_dynamic_children = false;
740
+ for (let i = context.path.length - 1; i >= 0; i--) {
741
+ const anc = context.path[i];
742
+ if (anc && anc.type === 'Component' && anc.metadata && anc.metadata.inherited_css) {
743
+ inside_dynamic_children = true;
744
+ break;
745
+ }
746
+ }
747
+ if (inside_dynamic_children) {
748
+ for (let i = context.path.length - 1; i >= 0; i--) {
749
+ const anc = context.path[i];
750
+ if (anc && anc.type === 'Component' && anc.css) {
751
+ scoping_hash = anc.css.hash;
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ }
757
+
685
758
  state.template.push(`<${node.id.name}`);
686
759
 
687
760
  for (const attr of node.attributes) {
@@ -851,8 +924,8 @@ const visitors = {
851
924
  if (class_attribute.value.type === 'Literal') {
852
925
  let value = class_attribute.value.value;
853
926
 
854
- if (node.metadata.scoped && state.component.css) {
855
- value = `${state.component.css.hash} ${value}`;
927
+ if (scoping_hash) {
928
+ value = `${scoping_hash} ${value}`;
856
929
  }
857
930
 
858
931
  handle_static_attr(class_attribute.name.name, value);
@@ -861,10 +934,7 @@ const visitors = {
861
934
  const metadata = { tracking: false, await: false };
862
935
  let expression = visit(class_attribute.value, { ...state, metadata });
863
936
 
864
- const hash_arg =
865
- node.metadata.scoped && state.component.css
866
- ? b.literal(state.component.css.hash)
867
- : undefined;
937
+ const hash_arg = scoping_hash ? b.literal(scoping_hash) : undefined;
868
938
  const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg';
869
939
 
870
940
  if (metadata.tracking) {
@@ -877,10 +947,8 @@ const visitors = {
877
947
  );
878
948
  }
879
949
  }
880
- } else if (node.metadata.scoped && state.component.css) {
881
- const value = state.component.css.hash;
882
-
883
- handle_static_attr(is_spreading ? '#class' : 'class', value);
950
+ } else if (scoping_hash) {
951
+ handle_static_attr(is_spreading ? '#class' : 'class', scoping_hash);
884
952
  }
885
953
 
886
954
  if (style_attribute !== null) {
@@ -1019,7 +1087,14 @@ const visitors = {
1019
1087
 
1020
1088
  if (children_filtered.length > 0) {
1021
1089
  const component_scope = context.state.scopes.get(node);
1022
- const children = visit(b.component(b.id('children'), [], children_filtered), {
1090
+ const children_component = b.component(b.id('children'), [], children_filtered);
1091
+
1092
+ children_component.metadata = {
1093
+ ...(children_component.metadata || {}),
1094
+ inherited_css: true,
1095
+ };
1096
+
1097
+ const children = visit(children_component, {
1023
1098
  ...context.state,
1024
1099
  scope: component_scope,
1025
1100
  namespace: child_namespace,
@@ -1143,7 +1218,7 @@ const visitors = {
1143
1218
  const operator = node.operator;
1144
1219
  const right = node.right;
1145
1220
 
1146
- if (operator !== '=') {
1221
+ if (operator !== '=' && context.state.metadata?.tracking === false) {
1147
1222
  context.state.metadata.tracking = true;
1148
1223
  }
1149
1224
 
@@ -1197,7 +1272,9 @@ const visitors = {
1197
1272
  (argument.tracked || (argument.property.type === 'Identifier' && argument.property.tracked))
1198
1273
  ) {
1199
1274
  add_ripple_internal_import(context);
1200
- context.state.metadata.tracking = true;
1275
+ if (context.state.metadata?.tracking === false) {
1276
+ context.state.metadata.tracking = true;
1277
+ }
1201
1278
 
1202
1279
  return b.call(
1203
1280
  node.prefix ? '_$_.update_pre_property' : '_$_.update_property',
@@ -1613,7 +1690,17 @@ function transform_ts_child(node, context) {
1613
1690
  state.init.push(b.stmt(visit(node.expression, { ...state })));
1614
1691
  } else if (node.type === 'Element') {
1615
1692
  // Use capitalized name for dynamic components/elements in TypeScript output
1616
- const type = node.metadata?.ts_name || node.id.name;
1693
+ // If node.id is not an Identifier (e.g., MemberExpression like props.children),
1694
+ // we need to visit it to get the proper expression
1695
+ let type_expression;
1696
+ let type_is_expression = false;
1697
+ if (node.id.type === 'MemberExpression') {
1698
+ // For MemberExpressions, we need to create a JSXExpression, not a JSXIdentifier
1699
+ type_expression = visit(node.id, state);
1700
+ type_is_expression = true;
1701
+ } else {
1702
+ type_expression = node.metadata?.ts_name || node.id.name;
1703
+ }
1617
1704
  const children = [];
1618
1705
  let has_children_props = false;
1619
1706
 
@@ -1651,9 +1738,7 @@ function transform_ts_child(node, context) {
1651
1738
  const createRefKeyAlias = import_from_ripple_if_needed('createRefKey', context);
1652
1739
  const metadata = { await: false };
1653
1740
  const argument = visit(attr.argument, { ...state, metadata });
1654
- const wrapper = b.object(
1655
- [b.prop('init', b.call(createRefKeyAlias), argument, true)]
1656
- );
1741
+ const wrapper = b.object([b.prop('init', b.call(createRefKeyAlias), argument, true)]);
1657
1742
  return b.jsx_spread_attribute(wrapper);
1658
1743
  }
1659
1744
  });
@@ -1678,33 +1763,40 @@ function transform_ts_child(node, context) {
1678
1763
  }
1679
1764
  }
1680
1765
 
1681
- const opening_type = b.jsx_id(type);
1682
- // Use node.id.loc if available, otherwise create a loc based on the element's position
1683
- opening_type.loc = node.id.loc || {
1684
- start: {
1685
- line: node.loc.start.line,
1686
- column: node.loc.start.column + 2, // After "<@"
1687
- },
1688
- end: {
1689
- line: node.loc.start.line,
1690
- column: node.loc.start.column + 2 + type.length,
1691
- },
1692
- };
1693
-
1694
- let closing_type = undefined;
1766
+ let opening_type, closing_type;
1695
1767
 
1696
- if (!node.selfClosing) {
1697
- closing_type = b.jsx_id(type);
1698
- closing_type.loc = {
1768
+ if (type_is_expression) {
1769
+ // For dynamic/expression-based components (e.g., props.children),
1770
+ // use JSX expression instead of identifier
1771
+ opening_type = type_expression;
1772
+ closing_type = node.selfClosing ? undefined : type_expression;
1773
+ } else {
1774
+ opening_type = b.jsx_id(type_expression);
1775
+ // Use node.id.loc if available, otherwise create a loc based on the element's position
1776
+ opening_type.loc = node.id.loc || {
1699
1777
  start: {
1700
- line: node.loc.end.line,
1701
- column: node.loc.end.column - type.length - 1,
1778
+ line: node.loc.start.line,
1779
+ column: node.loc.start.column + 2, // After "<@"
1702
1780
  },
1703
1781
  end: {
1704
- line: node.loc.end.line,
1705
- column: node.loc.end.column - 1,
1782
+ line: node.loc.start.line,
1783
+ column: node.loc.start.column + 2 + type_expression.length,
1706
1784
  },
1707
1785
  };
1786
+
1787
+ if (!node.selfClosing) {
1788
+ closing_type = b.jsx_id(type_expression);
1789
+ closing_type.loc = {
1790
+ start: {
1791
+ line: node.loc.end.line,
1792
+ column: node.loc.end.column - type_expression.length - 1,
1793
+ },
1794
+ end: {
1795
+ line: node.loc.end.line,
1796
+ column: node.loc.end.column - 1,
1797
+ },
1798
+ };
1799
+ }
1708
1800
  }
1709
1801
 
1710
1802
  const jsxElement = b.jsx_element(
@@ -1819,8 +1911,14 @@ function transform_ts_child(node, context) {
1819
1911
  state.init.push(component);
1820
1912
  } else if (node.type === 'BreakStatement') {
1821
1913
  state.init.push(b.break);
1822
- } else {
1914
+ } else if (node.type === 'TsxCompat') {
1915
+ const children = node.children
1916
+ .map((child) => visit(child, state))
1917
+ .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '');
1918
+
1823
1919
  debugger;
1920
+ state.init.push(b.stmt(b.jsx_fragment(children)));
1921
+ } else {
1824
1922
  throw new Error('TODO');
1825
1923
  }
1826
1924
  }
@@ -2077,6 +2175,56 @@ function create_tsx_with_typescript_support() {
2077
2175
  context.visit(node.typeAnnotation);
2078
2176
  context.write(')');
2079
2177
  },
2178
+ // Custom handler for TSMappedType: { [K in keyof T]: T[K] }
2179
+ TSMappedType(node, context) {
2180
+ context.write('{ ');
2181
+ if (node.readonly) {
2182
+ if (node.readonly === '+' || node.readonly === true) {
2183
+ context.write('readonly ');
2184
+ } else if (node.readonly === '-') {
2185
+ context.write('-readonly ');
2186
+ }
2187
+ }
2188
+ context.write('[');
2189
+ // Visit the entire type parameter (TSTypeParameter node)
2190
+ if (node.typeParameter) {
2191
+ context.visit(node.typeParameter);
2192
+ }
2193
+ context.write(']');
2194
+ if (node.optional) {
2195
+ if (node.optional === '+' || node.optional === true) {
2196
+ context.write('?');
2197
+ } else if (node.optional === '-') {
2198
+ context.write('-?');
2199
+ }
2200
+ }
2201
+ context.write(': ');
2202
+ // Visit the value type - could be either typeAnnotation or nameType
2203
+ if (node.typeAnnotation) {
2204
+ context.visit(node.typeAnnotation);
2205
+ } else if (node.nameType) {
2206
+ context.visit(node.nameType);
2207
+ }
2208
+ context.write(' }');
2209
+ },
2210
+ // Custom handler for TSTypeParameter: K in T (for mapped types)
2211
+ // acord ts has a bug where `in` is printed as `extends`, so we override it here
2212
+ TSTypeParameter(node, context) {
2213
+ // For mapped types, the name is just a string, not an Identifier node
2214
+ if (typeof node.name === 'string') {
2215
+ context.write(node.name);
2216
+ } else if (node.name && node.name.name) {
2217
+ context.write(node.name.name);
2218
+ }
2219
+ if (node.constraint) {
2220
+ context.write(' in ');
2221
+ context.visit(node.constraint);
2222
+ }
2223
+ if (node.default) {
2224
+ context.write(' = ');
2225
+ context.visit(node.default);
2226
+ }
2227
+ },
2080
2228
  // Override the ArrowFunctionExpression handler to support TypeScript return types
2081
2229
  ArrowFunctionExpression(node, context) {
2082
2230
  if (node.async) context.write('async ');
@@ -2108,7 +2256,7 @@ function create_tsx_with_typescript_support() {
2108
2256
  } else {
2109
2257
  context.visit(node.body);
2110
2258
  }
2111
- }
2259
+ },
2112
2260
  };
2113
2261
  }
2114
2262
 
@@ -2124,7 +2272,12 @@ export function transform_client(filename, source, analysis, to_ts) {
2124
2272
  const ripple_user_imports = new Map(); // exported -> local
2125
2273
  if (analysis && analysis.ast && Array.isArray(analysis.ast.body)) {
2126
2274
  for (const stmt of analysis.ast.body) {
2127
- if (stmt && stmt.type === 'ImportDeclaration' && stmt.source && stmt.source.value === 'ripple') {
2275
+ if (
2276
+ stmt &&
2277
+ stmt.type === 'ImportDeclaration' &&
2278
+ stmt.source &&
2279
+ stmt.source.value === 'ripple'
2280
+ ) {
2128
2281
  for (const spec of stmt.specifiers || []) {
2129
2282
  if (spec.type === 'ImportSpecifier' && spec.imported && spec.local) {
2130
2283
  ripple_user_imports.set(spec.imported.name, spec.local.name);
@@ -124,17 +124,32 @@ export function ref(element, get_fn) {
124
124
  }
125
125
 
126
126
  /**
127
- * @param {() => void} fn
127
+ * @param {() => (void | (() => void))} fn
128
128
  * @param {CompatOptions} [compat]
129
129
  * @returns {Block}
130
130
  */
131
131
  export function root(fn, compat) {
132
+ var target_fn = fn;
133
+
132
134
  if (compat != null) {
135
+ /** @type {Array<void | (() => void)>} */
136
+ var unmounts = [];
133
137
  for (var key in compat) {
134
138
  var api = compat[key];
135
- api.createRoot();
139
+ unmounts.push(api.createRoot());
136
140
  }
141
+ target_fn = () => {
142
+ var component_unmount = fn();
143
+
144
+ return () => {
145
+ component_unmount?.();
146
+ for (var unmount of unmounts) {
147
+ unmount?.();
148
+ }
149
+ };
150
+ };
137
151
  }
152
+
138
153
  return block(ROOT_BLOCK, fn, { compat });
139
154
  }
140
155
 
@@ -753,6 +753,23 @@ export function jsx_element(
753
753
  return element;
754
754
  }
755
755
 
756
+ /**
757
+ * @param {Array<ESTree.JSXText | ESTree.JSXExpressionContainer | ESTree.JSXSpreadChild | ESTree.JSXElement | ESTree.JSXFragment>} children
758
+ * @returns {ESTree.JSXFragment}
759
+ */
760
+ export function jsx_fragment(children = []) {
761
+ return {
762
+ type: 'JSXFragment',
763
+ openingFragment: {
764
+ type: 'JSXOpeningFragment',
765
+ },
766
+ closingFragment: {
767
+ type: 'JSXClosingFragment',
768
+ },
769
+ children,
770
+ };
771
+ }
772
+
756
773
  /**
757
774
  * @param {ESTree.Expression | ESTree.JSXEmptyExpression} expression
758
775
  * @returns {ESTree.JSXExpressionContainer}
@@ -3,7 +3,7 @@ import { track, flushSync } from 'ripple';
3
3
  describe('basic client > components & composition', () => {
4
4
  it('renders with component composition and children', () => {
5
5
  component Card(props) {
6
- <div class='card'>
6
+ <div class="card">
7
7
  <props.children />
8
8
  </div>
9
9
  }
@@ -27,16 +27,14 @@ describe('basic client > components & composition', () => {
27
27
 
28
28
  it('renders with nested components and prop passing', () => {
29
29
  component Button(props) {
30
- <button class={props.variant} onClick={props.onClick}>
31
- {props.label}
32
- </button>
30
+ <button class={props.variant} onClick={props.onClick}>{props.label}</button>
33
31
  }
34
32
 
35
33
  component Card(props) {
36
- <div class='card'>
34
+ <div class="card">
37
35
  <h3>{props.title}</h3>
38
36
  <p>{props.content}</p>
39
- <Button variant='primary' label={props.buttonText} onClick={props.onAction} />
37
+ <Button variant="primary" label={props.buttonText} onClick={props.onAction} />
40
38
  </div>
41
39
  }
42
40
 
@@ -44,12 +42,12 @@ describe('basic client > components & composition', () => {
44
42
  let clicked = track(false);
45
43
 
46
44
  <Card
47
- title='Test Card'
48
- content='This is a test card'
49
- buttonText='Click me'
45
+ title="Test Card"
46
+ content="This is a test card"
47
+ buttonText="Click me"
50
48
  onAction={() => @clicked = true}
51
49
  />
52
- <div class='status'>{@clicked ? 'Clicked' : 'Not clicked'}</div>
50
+ <div class="status">{@clicked ? 'Clicked' : 'Not clicked'}</div>
53
51
  }
54
52
 
55
53
  render(Basic);
@@ -74,8 +72,8 @@ describe('basic client > components & composition', () => {
74
72
 
75
73
  it('renders with reactive component props', () => {
76
74
  component ChildComponent(props) {
77
- <div class='child-content'>{props.@text}</div>
78
- <div class='child-count'>{props.@count}</div>
75
+ <div class="child-content">{props.@text}</div>
76
+ <div class="child-count">{props.@count}</div>
79
77
  }
80
78
 
81
79
  component Basic() {
@@ -83,10 +81,14 @@ describe('basic client > components & composition', () => {
83
81
  let number = track(1);
84
82
 
85
83
  <ChildComponent text={message} count={number} />
86
- <button onClick={() => {
87
- @message = @message === 'Hello' ? 'Goodbye' : 'Hello';
88
- @number++;
89
- }}>{'Update Props'}</button>
84
+ <button
85
+ onClick={() => {
86
+ @message = @message === 'Hello' ? 'Goodbye' : 'Hello';
87
+ @number++;
88
+ }}
89
+ >
90
+ {'Update Props'}
91
+ </button>
90
92
  }
91
93
 
92
94
  render(Basic);
@@ -139,7 +141,9 @@ describe('basic client > components & composition', () => {
139
141
  @hasError = true;
140
142
  }
141
143
  }}
142
- >{'Nonexistent'}</button>
144
+ >
145
+ {'Nonexistent'}
146
+ </button>
143
147
  <button
144
148
  onClick={() => {
145
149
  @hasError = false;
@@ -149,7 +153,9 @@ describe('basic client > components & composition', () => {
149
153
  @hasError = true;
150
154
  }
151
155
  }}
152
- >{'Nonexistent chaining'}</button>
156
+ >
157
+ {'Nonexistent chaining'}
158
+ </button>
153
159
  <button
154
160
  onClick={() => {
155
161
  @hasError = false;
@@ -159,7 +165,9 @@ describe('basic client > components & composition', () => {
159
165
  @hasError = true;
160
166
  }
161
167
  }}
162
- >{'Object null'}</button>
168
+ >
169
+ {'Object null'}
170
+ </button>
163
171
  <button
164
172
  onClick={() => {
165
173
  @hasError = false;
@@ -169,7 +177,9 @@ describe('basic client > components & composition', () => {
169
177
  @hasError = true;
170
178
  }
171
179
  }}
172
- >{'Object null chained'}</button>
180
+ >
181
+ {'Object null chained'}
182
+ </button>
173
183
  <button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
174
184
 
175
185
  <span>{obj.@count}</span>