ripple 0.3.10 → 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 (39) hide show
  1. package/CHANGELOG.md +31 -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 +170 -8
  6. package/src/compiler/phases/2-analyze/index.js +231 -20
  7. package/src/compiler/phases/3-transform/client/index.js +169 -77
  8. package/src/compiler/phases/3-transform/server/index.js +46 -3
  9. package/src/compiler/types/index.d.ts +19 -2
  10. package/src/compiler/types/parse.d.ts +1 -1
  11. package/src/compiler/utils.js +174 -0
  12. package/src/runtime/index-client.js +14 -4
  13. package/src/runtime/internal/client/composite.js +2 -2
  14. package/src/runtime/internal/client/expression.js +64 -2
  15. package/src/runtime/internal/client/portal.js +1 -1
  16. package/src/utils/builders.js +30 -0
  17. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  18. package/tests/client/basic/basic.rendering.test.ripple +4 -2
  19. package/tests/client/composite/composite.render.test.ripple +46 -0
  20. package/tests/client/return.test.ripple +101 -0
  21. package/tests/client/tsx.test.ripple +486 -0
  22. package/tests/hydration/compiled/client/basic.js +8 -24
  23. package/tests/hydration/compiled/client/composite.js +6 -24
  24. package/tests/hydration/compiled/client/events.js +9 -54
  25. package/tests/hydration/compiled/client/for.js +59 -152
  26. package/tests/hydration/compiled/client/head.js +5 -20
  27. package/tests/hydration/compiled/client/hmr.js +2 -8
  28. package/tests/hydration/compiled/client/html.js +59 -226
  29. package/tests/hydration/compiled/client/if-children.js +6 -22
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +18 -66
  31. package/tests/hydration/compiled/client/nested-control-flow.js +92 -368
  32. package/tests/hydration/compiled/client/portal.js +4 -16
  33. package/tests/hydration/compiled/client/reactivity.js +7 -40
  34. package/tests/hydration/compiled/client/return.js +1 -4
  35. package/tests/hydration/compiled/client/try.js +2 -2
  36. package/tests/utils/compiler-compat-config.test.js +38 -0
  37. package/tests/utils/vite-plugin-config.test.js +113 -0
  38. package/tsconfig.typecheck.json +2 -1
  39. package/types/index.d.ts +2 -12
@@ -41,6 +41,123 @@ import { validate_nesting } from './validation.js';
41
41
 
42
42
  const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
43
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
+
44
161
  /**
45
162
  * @param {AnalysisContext['path']} path
46
163
  */
@@ -63,6 +180,7 @@ function mark_control_flow_has_template(path) {
63
180
  node.type === 'TryStatement' ||
64
181
  node.type === 'IfStatement' ||
65
182
  node.type === 'SwitchStatement' ||
183
+ node.type === 'Tsx' ||
66
184
  node.type === 'TsxCompat'
67
185
  ) {
68
186
  node.metadata.has_template = true;
@@ -125,7 +243,11 @@ function setup_lazy_transforms(pattern, source_id, state, writable, is_track_cal
125
243
 
126
244
  if (node.prefix) {
127
245
  // ++count: return new value
128
- 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
+ );
129
251
  } else {
130
252
  // count++: return old value, write new value
131
253
  // Use IIFE to declare temp variable
@@ -135,7 +257,13 @@ function setup_lazy_transforms(pattern, source_id, state, writable, is_track_cal
135
257
  [],
136
258
  b.block([
137
259
  b.var(temp, fallback_read),
138
- 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
+ ),
139
267
  b.return(temp),
140
268
  ]),
141
269
  ),
@@ -198,7 +326,12 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
198
326
  if (i === 0) {
199
327
  // Fast path for index 0: use _$_.get(source) instead of source[0]
200
328
  const read_expr = has_fallback
201
- ? () => 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
+ )
202
335
  : () => b.call('_$_.get', source_id);
203
336
 
204
337
  // Signal that read already produces an unwrapped value (calls _$_.get internally)
@@ -247,6 +380,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
247
380
  } else {
248
381
  binding.transform.update = (node) => {
249
382
  const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
383
+ /** @type {AST.Expression[]} */
250
384
  const args = [source_id];
251
385
  if (node.operator === '--') {
252
386
  args.push(b.literal(-1));
@@ -280,7 +414,10 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
280
414
  binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
281
415
 
282
416
  binding.transform = {
283
- read: (_) => path.expression(base_expression(source_id)),
417
+ read: (_) =>
418
+ path.expression(
419
+ /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
420
+ ),
284
421
  };
285
422
 
286
423
  if (writable) {
@@ -288,7 +425,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
288
425
  return b.assignment(
289
426
  '=',
290
427
  /** @type {AST.MemberExpression} */ (
291
- path.update_expression(base_expression(source_id))
428
+ path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id)))
292
429
  ),
293
430
  value,
294
431
  );
@@ -296,12 +433,20 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
296
433
 
297
434
  if (path.has_default_value) {
298
435
  binding.transform.update = (node) => {
299
- const member = path.update_expression(base_expression(source_id));
300
- 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
+ );
301
442
  const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
302
443
 
303
444
  if (node.prefix) {
304
- 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
+ );
305
450
  } else {
306
451
  const temp = b.id('_v');
307
452
  return b.call(
@@ -309,7 +454,13 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
309
454
  [],
310
455
  b.block([
311
456
  b.var(temp, fallback_read),
312
- 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
+ ),
313
464
  b.return(temp),
314
465
  ]),
315
466
  ),
@@ -320,7 +471,7 @@ function setup_lazy_array_transforms(pattern, source_id, state, writable) {
320
471
  binding.transform.update = (node) =>
321
472
  b.update(
322
473
  node.operator,
323
- path.update_expression(base_expression(source_id)),
474
+ path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id))),
324
475
  node.prefix,
325
476
  );
326
477
  }
@@ -348,11 +499,11 @@ function unwrap_type_annotation(type_annotation) {
348
499
 
349
500
  while (annotation) {
350
501
  if (annotation.type === 'TSParenthesizedType') {
351
- annotation = annotation.typeAnnotation;
502
+ annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
352
503
  continue;
353
504
  }
354
505
  if (annotation.type === 'TSOptionalType') {
355
- annotation = annotation.typeAnnotation;
506
+ annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
356
507
  continue;
357
508
  }
358
509
  break;
@@ -375,11 +526,11 @@ function normalize_tuple_element_type(type_annotation) {
375
526
  continue;
376
527
  }
377
528
  if (annotation.type === 'TSParenthesizedType') {
378
- annotation = annotation.typeAnnotation;
529
+ annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
379
530
  continue;
380
531
  }
381
532
  if (annotation.type === 'TSOptionalType') {
382
- annotation = annotation.typeAnnotation;
533
+ annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
383
534
  continue;
384
535
  }
385
536
  break;
@@ -431,7 +582,7 @@ function get_object_property_type_annotation(type_annotation, property) {
431
582
  return undefined;
432
583
  }
433
584
 
434
- const key_name = get_object_pattern_key_name(property.key);
585
+ const key_name = get_object_pattern_key_name(/** @type {AST.Expression} */ (property.key));
435
586
  if (key_name === null) {
436
587
  return undefined;
437
588
  }
@@ -928,7 +1079,9 @@ const visitors = {
928
1079
  const callee = node.callee;
929
1080
 
930
1081
  if (
931
- !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
+ ) &&
932
1085
  is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
933
1086
  ) {
934
1087
  error(
@@ -1415,6 +1568,7 @@ const visitors = {
1415
1568
  ...node.metadata,
1416
1569
  has_template: false,
1417
1570
  has_await: false,
1571
+ has_throw: false,
1418
1572
  };
1419
1573
 
1420
1574
  const test_metadata = { tracking: false };
@@ -1436,7 +1590,7 @@ const visitors = {
1436
1590
  node.metadata.lone_return = true;
1437
1591
  }
1438
1592
 
1439
- if (!node.metadata.has_template && !node.metadata.has_return) {
1593
+ if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1440
1594
  error(
1441
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.',
1442
1596
  context.state.analysis.module.filename,
@@ -1451,9 +1605,10 @@ const visitors = {
1451
1605
  const saved_returns = node.metadata.returns;
1452
1606
  node.metadata.has_template = false;
1453
1607
  node.metadata.has_await = false;
1608
+ node.metadata.has_throw = false;
1454
1609
  context.visit(node.alternate, context.state);
1455
1610
 
1456
- if (!node.metadata.has_template && !node.metadata.has_return) {
1611
+ if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1457
1612
  error(
1458
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.',
1459
1614
  context.state.analysis.module.filename,
@@ -1522,6 +1677,33 @@ const visitors = {
1522
1677
  }
1523
1678
  },
1524
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
+
1525
1707
  TryStatement(node, context) {
1526
1708
  const { state } = context;
1527
1709
  if (!is_inside_component(context)) {
@@ -1617,7 +1799,7 @@ const visitors = {
1617
1799
  },
1618
1800
 
1619
1801
  JSXElement(node, context) {
1620
- 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');
1621
1803
 
1622
1804
  if (inside_tsx_compat) {
1623
1805
  return context.next();
@@ -1630,11 +1812,28 @@ const visitors = {
1630
1812
  );
1631
1813
  },
1632
1814
 
1633
- TsxCompat(_, context) {
1815
+ Tsx(_, context) {
1634
1816
  mark_control_flow_has_template(context.path);
1635
1817
  return context.next();
1636
1818
  },
1637
1819
 
1820
+ TsxCompat(node, context) {
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
+
1834
+ return context.next();
1835
+ },
1836
+
1638
1837
  Element(node, context) {
1639
1838
  if (!is_inside_component(context)) {
1640
1839
  error(
@@ -1910,6 +2109,16 @@ const visitors = {
1910
2109
  RippleExpression(node, context) {
1911
2110
  mark_control_flow_has_template(context.path);
1912
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
+
1913
2122
  context.next();
1914
2123
  },
1915
2124
 
@@ -2017,6 +2226,8 @@ export function analyze(ast, filename, options = {}) {
2017
2226
  ancestor_server_block: undefined,
2018
2227
  to_ts: options.to_ts ?? false,
2019
2228
  loose,
2229
+ configured_compat_kinds:
2230
+ options.compat_kinds === undefined ? undefined : new Set(options.compat_kinds),
2020
2231
  metadata: {},
2021
2232
  mode: options.mode,
2022
2233
  },