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
@@ -26,6 +26,7 @@ import {
26
26
  is_inside_component,
27
27
  is_ripple_track_call,
28
28
  is_void_element,
29
+ is_children_template_expression as is_children_template_expression_in_scope,
29
30
  normalize_children,
30
31
  is_binding_function,
31
32
  is_inside_try_block,
@@ -40,6 +41,123 @@ import { validate_nesting } from './validation.js';
40
41
 
41
42
  const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
42
43
 
44
+ const mutating_method_names = new Set([
45
+ 'add',
46
+ 'append',
47
+ 'clear',
48
+ 'copyWithin',
49
+ 'delete',
50
+ 'fill',
51
+ 'pop',
52
+ 'push',
53
+ 'reverse',
54
+ 'set',
55
+ 'shift',
56
+ 'sort',
57
+ 'splice',
58
+ 'unshift',
59
+ ]);
60
+
61
+ /**
62
+ * @param {AST.MemberExpression} node
63
+ * @returns {string | null}
64
+ */
65
+ function get_member_name(node) {
66
+ if (!node.computed && node.property.type === 'Identifier') {
67
+ return node.property.name;
68
+ }
69
+
70
+ if (node.computed && node.property.type === 'Literal') {
71
+ return typeof node.property.value === 'string' ? node.property.value : null;
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * @param {AST.CallExpression} node
79
+ * @returns {boolean}
80
+ */
81
+ function is_mutating_call_expression(node) {
82
+ return (
83
+ node.callee.type === 'MemberExpression' &&
84
+ mutating_method_names.has(get_member_name(node.callee) ?? '')
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Check if an expression contains side effects or other impure operations.
90
+ * Template expressions should be pure reads.
91
+ * @param {AST.Expression | AST.SpreadElement | AST.Super | AST.Pattern} node
92
+ * @returns {boolean}
93
+ */
94
+ function expression_has_side_effects(node) {
95
+ switch (node.type) {
96
+ case 'AssignmentExpression':
97
+ case 'UpdateExpression':
98
+ return true;
99
+ case 'SequenceExpression':
100
+ return node.expressions.some(expression_has_side_effects);
101
+ case 'ConditionalExpression':
102
+ return (
103
+ expression_has_side_effects(node.test) ||
104
+ expression_has_side_effects(node.consequent) ||
105
+ expression_has_side_effects(node.alternate)
106
+ );
107
+ case 'LogicalExpression':
108
+ case 'BinaryExpression':
109
+ return (
110
+ expression_has_side_effects(/** @type {AST.Expression} */ (node.left)) ||
111
+ expression_has_side_effects(node.right)
112
+ );
113
+ case 'UnaryExpression':
114
+ // delete operator has side effects (removes object properties)
115
+ if (node.operator === 'delete') return true;
116
+ return expression_has_side_effects(node.argument);
117
+ case 'AwaitExpression':
118
+ return expression_has_side_effects(node.argument);
119
+ case 'ChainExpression':
120
+ return expression_has_side_effects(node.expression);
121
+ case 'MemberExpression':
122
+ return (
123
+ expression_has_side_effects(node.object) ||
124
+ (node.computed &&
125
+ expression_has_side_effects(/** @type {AST.Expression} */ (node.property)))
126
+ );
127
+ case 'CallExpression':
128
+ return (
129
+ is_mutating_call_expression(node) ||
130
+ expression_has_side_effects(node.callee) ||
131
+ node.arguments.some(expression_has_side_effects)
132
+ );
133
+ case 'NewExpression':
134
+ return (
135
+ expression_has_side_effects(node.callee) || node.arguments.some(expression_has_side_effects)
136
+ );
137
+ case 'TemplateLiteral':
138
+ return node.expressions.some(expression_has_side_effects);
139
+ case 'TaggedTemplateExpression':
140
+ return (
141
+ expression_has_side_effects(node.tag) ||
142
+ node.quasi.expressions.some(expression_has_side_effects)
143
+ );
144
+ case 'ArrayExpression':
145
+ return node.elements.some((el) => el !== null && expression_has_side_effects(el));
146
+ case 'ObjectExpression':
147
+ return node.properties.some((prop) =>
148
+ prop.type === 'SpreadElement'
149
+ ? expression_has_side_effects(prop.argument)
150
+ : expression_has_side_effects(prop.value) ||
151
+ (prop.computed &&
152
+ expression_has_side_effects(/** @type {AST.Expression} */ (prop.key))),
153
+ );
154
+ case 'SpreadElement':
155
+ return expression_has_side_effects(node.argument);
156
+ default:
157
+ return false;
158
+ }
159
+ }
160
+
43
161
  /**
44
162
  * @param {AnalysisContext['path']} path
45
163
  */
@@ -62,6 +180,7 @@ function mark_control_flow_has_template(path) {
62
180
  node.type === 'TryStatement' ||
63
181
  node.type === 'IfStatement' ||
64
182
  node.type === 'SwitchStatement' ||
183
+ node.type === 'Tsx' ||
65
184
  node.type === 'TsxCompat'
66
185
  ) {
67
186
  node.metadata.has_template = true;
@@ -124,7 +243,11 @@ function setup_lazy_transforms(pattern, source_id, state, writable, is_track_cal
124
243
 
125
244
  if (node.prefix) {
126
245
  // ++count: return new value
127
- return b.assignment('=', member, b.binary('+', fallback_read, delta));
246
+ return b.assignment(
247
+ '=',
248
+ /** @type {AST.Pattern} */ (member),
249
+ b.binary('+', fallback_read, delta),
250
+ );
128
251
  } else {
129
252
  // count++: return old value, write new value
130
253
  // Use IIFE to declare temp variable
@@ -134,7 +257,13 @@ function setup_lazy_transforms(pattern, source_id, state, writable, is_track_cal
134
257
  [],
135
258
  b.block([
136
259
  b.var(temp, fallback_read),
137
- b.stmt(b.assignment('=', member, b.binary('+', temp, delta))),
260
+ b.stmt(
261
+ b.assignment(
262
+ '=',
263
+ /** @type {AST.Pattern} */ (member),
264
+ b.binary('+', temp, delta),
265
+ ),
266
+ ),
138
267
  b.return(temp),
139
268
  ]),
140
269
  ),
@@ -197,7 +326,12 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
197
326
  if (i === 0) {
198
327
  // Fast path for index 0: use _$_.get(source) instead of source[0]
199
328
  const read_expr = has_fallback
200
- ? () => b.call('_$_.fallback', b.call('_$_.get', source_id), fallback_value)
329
+ ? () =>
330
+ b.call(
331
+ '_$_.fallback',
332
+ b.call('_$_.get', source_id),
333
+ /** @type {AST.Expression} */ (fallback_value),
334
+ )
201
335
  : () => b.call('_$_.get', source_id);
202
336
 
203
337
  // Signal that read already produces an unwrapped value (calls _$_.get internally)
@@ -246,6 +380,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
246
380
  } else {
247
381
  binding.transform.update = (node) => {
248
382
  const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
383
+ /** @type {AST.Expression[]} */
249
384
  const args = [source_id];
250
385
  if (node.operator === '--') {
251
386
  args.push(b.literal(-1));
@@ -279,7 +414,10 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
279
414
  binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
280
415
 
281
416
  binding.transform = {
282
- read: (_) => path.expression(base_expression(source_id)),
417
+ read: (_) =>
418
+ path.expression(
419
+ /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
420
+ ),
283
421
  };
284
422
 
285
423
  if (writable) {
@@ -287,7 +425,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
287
425
  return b.assignment(
288
426
  '=',
289
427
  /** @type {AST.MemberExpression} */ (
290
- path.update_expression(base_expression(source_id))
428
+ path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id)))
291
429
  ),
292
430
  value,
293
431
  );
@@ -295,12 +433,20 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
295
433
 
296
434
  if (path.has_default_value) {
297
435
  binding.transform.update = (node) => {
298
- const member = path.update_expression(base_expression(source_id));
299
- const fallback_read = path.expression(base_expression(source_id));
436
+ const member = path.update_expression(
437
+ /** @type {AST.Identifier} */ (base_expression(source_id)),
438
+ );
439
+ const fallback_read = path.expression(
440
+ /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
441
+ );
300
442
  const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
301
443
 
302
444
  if (node.prefix) {
303
- return b.assignment('=', member, b.binary('+', fallback_read, delta));
445
+ return b.assignment(
446
+ '=',
447
+ /** @type {AST.Pattern} */ (member),
448
+ b.binary('+', fallback_read, delta),
449
+ );
304
450
  } else {
305
451
  const temp = b.id('_v');
306
452
  return b.call(
@@ -308,7 +454,13 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
308
454
  [],
309
455
  b.block([
310
456
  b.var(temp, fallback_read),
311
- b.stmt(b.assignment('=', member, b.binary('+', temp, delta))),
457
+ b.stmt(
458
+ b.assignment(
459
+ '=',
460
+ /** @type {AST.Pattern} */ (member),
461
+ b.binary('+', temp, delta),
462
+ ),
463
+ ),
312
464
  b.return(temp),
313
465
  ]),
314
466
  ),
@@ -319,7 +471,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
319
471
  binding.transform.update = (node) =>
320
472
  b.update(
321
473
  node.operator,
322
- path.update_expression(base_expression(source_id)),
474
+ path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id))),
323
475
  node.prefix,
324
476
  );
325
477
  }
@@ -347,11 +499,11 @@ function unwrap_type_annotation(type_annotation) {
347
499
 
348
500
  while (annotation) {
349
501
  if (annotation.type === 'TSParenthesizedType') {
350
- annotation = annotation.typeAnnotation;
502
+ annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
351
503
  continue;
352
504
  }
353
505
  if (annotation.type === 'TSOptionalType') {
354
- annotation = annotation.typeAnnotation;
506
+ annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
355
507
  continue;
356
508
  }
357
509
  break;
@@ -374,11 +526,11 @@ function normalize_tuple_element_type(type_annotation) {
374
526
  continue;
375
527
  }
376
528
  if (annotation.type === 'TSParenthesizedType') {
377
- annotation = annotation.typeAnnotation;
529
+ annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
378
530
  continue;
379
531
  }
380
532
  if (annotation.type === 'TSOptionalType') {
381
- annotation = annotation.typeAnnotation;
533
+ annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
382
534
  continue;
383
535
  }
384
536
  break;
@@ -430,7 +582,7 @@ function get_object_property_type_annotation(type_annotation, property) {
430
582
  return undefined;
431
583
  }
432
584
 
433
- const key_name = get_object_pattern_key_name(property.key);
585
+ const key_name = get_object_pattern_key_name(/** @type {AST.Expression} */ (property.key));
434
586
  if (key_name === null) {
435
587
  return undefined;
436
588
  }
@@ -687,37 +839,6 @@ function error_return_keyword(node, context, message) {
687
839
  );
688
840
  }
689
841
 
690
- /**
691
- * @param {AST.Expression} expression
692
- * @returns {AST.Expression}
693
- */
694
- function unwrap_template_expression(expression) {
695
- /** @type {AST.Expression} */
696
- let node = expression;
697
-
698
- while (true) {
699
- if (
700
- node.type === 'ParenthesizedExpression' ||
701
- node.type === 'TSAsExpression' ||
702
- node.type === 'TSSatisfiesExpression' ||
703
- node.type === 'TSNonNullExpression' ||
704
- node.type === 'TSInstantiationExpression'
705
- ) {
706
- node = /** @type {AST.Expression} */ (node.expression);
707
- continue;
708
- }
709
-
710
- if (node.type === 'ChainExpression') {
711
- node = /** @type {AST.Expression} */ (node.expression);
712
- continue;
713
- }
714
-
715
- break;
716
- }
717
-
718
- return node;
719
- }
720
-
721
842
  /**
722
843
  * @param {AST.Expression} expression
723
844
  * @param {Context<AST.Node, AnalysisState>} context
@@ -726,44 +847,7 @@ function unwrap_template_expression(expression) {
726
847
  function is_children_template_expression(expression, context) {
727
848
  const component = context.path.findLast((node) => node.type === 'Component');
728
849
  const component_scope = component ? context.state.scopes.get(component) : null;
729
- const unwrapped = unwrap_template_expression(expression);
730
-
731
- if (unwrapped.type === 'MemberExpression') {
732
- let property_name = null;
733
-
734
- if (!unwrapped.computed && unwrapped.property.type === 'Identifier') {
735
- property_name = unwrapped.property.name;
736
- } else if (
737
- unwrapped.computed &&
738
- unwrapped.property.type === 'Literal' &&
739
- typeof unwrapped.property.value === 'string'
740
- ) {
741
- property_name = unwrapped.property.value;
742
- }
743
-
744
- if (property_name === 'children') {
745
- const target = unwrap_template_expression(/** @type {AST.Expression} */ (unwrapped.object));
746
-
747
- if (target.type === 'Identifier') {
748
- const binding = context.state.scope.get(target.name);
749
- return binding?.declaration_kind === 'param' && binding.scope === component_scope;
750
- }
751
- }
752
- }
753
-
754
- if (unwrapped.type !== 'Identifier' || unwrapped.name !== 'children') {
755
- return false;
756
- }
757
-
758
- const binding = context.state.scope.get(unwrapped.name);
759
- return (
760
- (binding?.declaration_kind === 'param' ||
761
- binding?.kind === 'prop' ||
762
- binding?.kind === 'prop_fallback' ||
763
- binding?.kind === 'lazy' ||
764
- binding?.kind === 'lazy_fallback') &&
765
- binding.scope === component_scope
766
- );
850
+ return is_children_template_expression_in_scope(expression, context.state.scope, component_scope);
767
851
  }
768
852
 
769
853
  /** @type {Visitors<AST.Node, AnalysisState>} */
@@ -995,11 +1079,13 @@ const visitors = {
995
1079
  const callee = node.callee;
996
1080
 
997
1081
  if (
998
- !context.path.some((path_node) => path_node.type === 'TsxCompat') &&
1082
+ !context.path.some(
1083
+ (path_node) => path_node.type === 'TsxCompat' || path_node.type === 'Tsx',
1084
+ ) &&
999
1085
  is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
1000
1086
  ) {
1001
1087
  error(
1002
- '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
1088
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
1003
1089
  context.state.analysis.module.filename,
1004
1090
  callee,
1005
1091
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -1482,6 +1568,7 @@ const visitors = {
1482
1568
  ...node.metadata,
1483
1569
  has_template: false,
1484
1570
  has_await: false,
1571
+ has_throw: false,
1485
1572
  };
1486
1573
 
1487
1574
  const test_metadata = { tracking: false };
@@ -1503,7 +1590,7 @@ const visitors = {
1503
1590
  node.metadata.lone_return = true;
1504
1591
  }
1505
1592
 
1506
- if (!node.metadata.has_template && !node.metadata.has_return) {
1593
+ if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1507
1594
  error(
1508
1595
  'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
1509
1596
  context.state.analysis.module.filename,
@@ -1518,9 +1605,10 @@ const visitors = {
1518
1605
  const saved_returns = node.metadata.returns;
1519
1606
  node.metadata.has_template = false;
1520
1607
  node.metadata.has_await = false;
1608
+ node.metadata.has_throw = false;
1521
1609
  context.visit(node.alternate, context.state);
1522
1610
 
1523
- if (!node.metadata.has_template && !node.metadata.has_return) {
1611
+ if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1524
1612
  error(
1525
1613
  'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
1526
1614
  context.state.analysis.module.filename,
@@ -1589,6 +1677,33 @@ const visitors = {
1589
1677
  }
1590
1678
  },
1591
1679
 
1680
+ ThrowStatement(node, context) {
1681
+ if (!is_inside_component(context)) {
1682
+ return context.next();
1683
+ }
1684
+
1685
+ for (let i = context.path.length - 1; i >= 0; i--) {
1686
+ const ancestor = context.path[i];
1687
+
1688
+ if (
1689
+ ancestor.type === 'Component' ||
1690
+ ancestor.type === 'FunctionExpression' ||
1691
+ ancestor.type === 'ArrowFunctionExpression' ||
1692
+ ancestor.type === 'FunctionDeclaration'
1693
+ ) {
1694
+ break;
1695
+ }
1696
+
1697
+ if (ancestor.type === 'IfStatement') {
1698
+ if (!ancestor.metadata.has_throw) {
1699
+ ancestor.metadata.has_throw = true;
1700
+ }
1701
+ }
1702
+ }
1703
+
1704
+ context.next();
1705
+ },
1706
+
1592
1707
  TryStatement(node, context) {
1593
1708
  const { state } = context;
1594
1709
  if (!is_inside_component(context)) {
@@ -1684,7 +1799,7 @@ const visitors = {
1684
1799
  },
1685
1800
 
1686
1801
  JSXElement(node, context) {
1687
- const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat');
1802
+ const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1688
1803
 
1689
1804
  if (inside_tsx_compat) {
1690
1805
  return context.next();
@@ -1697,8 +1812,25 @@ const visitors = {
1697
1812
  );
1698
1813
  },
1699
1814
 
1700
- TsxCompat(_, context) {
1815
+ Tsx(_, context) {
1816
+ mark_control_flow_has_template(context.path);
1817
+ return context.next();
1818
+ },
1819
+
1820
+ TsxCompat(node, context) {
1701
1821
  mark_control_flow_has_template(context.path);
1822
+
1823
+ const configured_compat_kinds = context.state.configured_compat_kinds;
1824
+ if (configured_compat_kinds !== undefined && !configured_compat_kinds.has(node.kind)) {
1825
+ error(
1826
+ `<tsx:${node.kind}> requires "${node.kind}" compat to be configured in ripple.config.ts.`,
1827
+ context.state.analysis.module.filename,
1828
+ node,
1829
+ context.state.loose ? context.state.analysis.errors : undefined,
1830
+ context.state.analysis.comments,
1831
+ );
1832
+ }
1833
+
1702
1834
  return context.next();
1703
1835
  },
1704
1836
 
@@ -1718,6 +1850,19 @@ const visitors = {
1718
1850
 
1719
1851
  mark_control_flow_has_template(path);
1720
1852
 
1853
+ if (
1854
+ !is_dom_element &&
1855
+ is_children_template_expression(/** @type {AST.Expression} */ (node.id), context)
1856
+ ) {
1857
+ error(
1858
+ '`children` cannot be rendered as a component. Render it with `{children}` or `{props.children}` instead.',
1859
+ state.analysis.module.filename,
1860
+ node.id,
1861
+ context.state.loose ? context.state.analysis.errors : undefined,
1862
+ context.state.analysis.comments,
1863
+ );
1864
+ }
1865
+
1721
1866
  validate_nesting(node, context);
1722
1867
 
1723
1868
  // Store capitalized name for dynamic components/elements
@@ -1771,7 +1916,10 @@ const visitors = {
1771
1916
  if (/** @type {AST.Identifier} */ (node.id).name === 'title') {
1772
1917
  const children = normalize_children(node.children, context);
1773
1918
 
1774
- if (children.length !== 1 || children[0].type !== 'Text') {
1919
+ if (
1920
+ children.length !== 1 ||
1921
+ (children[0].type !== 'RippleExpression' && children[0].type !== 'Text')
1922
+ ) {
1775
1923
  // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1776
1924
  error(
1777
1925
  '<title> must have only contain text nodes',
@@ -1916,30 +2064,22 @@ const visitors = {
1916
2064
  }
1917
2065
  /** @type {(AST.Node | AST.Expression)[]} */
1918
2066
  let implicit_children = [];
1919
- /** @type {AST.Identifier[]} */
1920
- let explicit_children = [];
1921
2067
 
1922
2068
  for (const child of node.children) {
1923
2069
  if (child.type === 'Component') {
1924
- if (child.id?.name === 'children') {
1925
- explicit_children.push(child.id);
1926
- }
1927
- } else if (child.type !== 'EmptyStatement') {
1928
- implicit_children.push(
1929
- child.type === 'Text' || child.type === 'Html' ? child.expression : child,
1930
- );
1931
- }
1932
- }
1933
-
1934
- if (implicit_children.length > 0 && explicit_children.length > 0) {
1935
- for (const item of [...explicit_children, ...implicit_children]) {
1936
2070
  error(
1937
- 'Cannot have both implicit and explicit children',
2071
+ 'Component declarations cannot be used inside composite component children. Pass them as explicit props on the template element instead.',
1938
2072
  state.analysis.module.filename,
1939
- item,
2073
+ child.id || child,
1940
2074
  context.state.loose ? context.state.analysis.errors : undefined,
1941
2075
  context.state.analysis.comments,
1942
2076
  );
2077
+ } else if (child.type !== 'EmptyStatement') {
2078
+ implicit_children.push(
2079
+ child.type === 'RippleExpression' || child.type === 'Text' || child.type === 'Html'
2080
+ ? child.expression
2081
+ : child,
2082
+ );
1943
2083
  }
1944
2084
  }
1945
2085
  }
@@ -1966,12 +2106,28 @@ const visitors = {
1966
2106
  };
1967
2107
  },
1968
2108
 
2109
+ RippleExpression(node, context) {
2110
+ mark_control_flow_has_template(context.path);
2111
+
2112
+ if (expression_has_side_effects(node.expression)) {
2113
+ error(
2114
+ 'Template expressions must not contain side effects.',
2115
+ context.state.analysis.module.filename,
2116
+ node.expression,
2117
+ context.state.loose ? context.state.analysis.errors : undefined,
2118
+ context.state.analysis.comments,
2119
+ );
2120
+ }
2121
+
2122
+ context.next();
2123
+ },
2124
+
1969
2125
  Text(node, context) {
1970
2126
  mark_control_flow_has_template(context.path);
1971
2127
 
1972
2128
  if (is_children_template_expression(/** @type {AST.Expression} */ (node.expression), context)) {
1973
2129
  error(
1974
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
2130
+ '`children` cannot be rendered using explicit text interpolation. Use `{children}` or `{props.children}` instead.',
1975
2131
  context.state.analysis.module.filename,
1976
2132
  node.expression,
1977
2133
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -2070,6 +2226,8 @@ export function analyze(ast, filename, options = {}) {
2070
2226
  ancestor_server_block: undefined,
2071
2227
  to_ts: options.to_ts ?? false,
2072
2228
  loose,
2229
+ configured_compat_kinds:
2230
+ options.compat_kinds === undefined ? undefined : new Set(options.compat_kinds),
2073
2231
  metadata: {},
2074
2232
  mode: options.mode,
2075
2233
  },
@@ -306,12 +306,20 @@ function get_descendant_elements(node, adjacent_only) {
306
306
  }
307
307
  }
308
308
 
309
- // For template nodes and text interpolations
309
+ // For template nodes and interpolation expressions
310
310
  if (
311
- /** @type {AST.TextNode} */ (current_node).expression &&
312
- typeof (/** @type {AST.TextNode} */ (current_node).expression) === 'object'
311
+ (current_node.type === 'RippleExpression' ||
312
+ current_node.type === 'Text' ||
313
+ current_node.type === 'Html') &&
314
+ /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression &&
315
+ typeof (
316
+ /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression
317
+ ) === 'object'
313
318
  ) {
314
- visit(/** @type {AST.TextNode} */ (current_node).expression, depth + 1);
319
+ visit(
320
+ /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (current_node).expression,
321
+ depth + 1,
322
+ );
315
323
  }
316
324
  }
317
325
 
@@ -409,7 +417,7 @@ function get_possible_element_siblings(node, direction, adjacent_only) {
409
417
  // Stop at non-whitespace text nodes for adjacent selectors
410
418
  else if (
411
419
  adjacent_only &&
412
- sibling.type === 'Text' &&
420
+ (sibling.type === 'RippleExpression' || sibling.type === 'Text') &&
413
421
  sibling.expression.type === 'Literal' &&
414
422
  typeof sibling.expression.value === 'string' &&
415
423
  sibling.expression.value.trim()