ripple 0.3.9 → 0.3.10

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 (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +25 -15
  4. package/src/compiler/phases/2-analyze/index.js +35 -88
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +188 -56
  7. package/src/compiler/phases/3-transform/server/index.js +62 -40
  8. package/src/compiler/types/index.d.ts +9 -1
  9. package/src/compiler/types/parse.d.ts +2 -0
  10. package/src/compiler/utils.js +101 -1
  11. package/src/runtime/element.js +39 -0
  12. package/src/runtime/internal/client/composite.js +10 -6
  13. package/src/runtime/internal/client/expression.js +218 -0
  14. package/src/runtime/internal/client/index.js +4 -0
  15. package/src/runtime/internal/client/portal.js +12 -6
  16. package/src/runtime/internal/server/index.js +26 -1
  17. package/tests/client/basic/basic.components.test.ripple +85 -87
  18. package/tests/client/basic/basic.errors.test.ripple +4 -8
  19. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  20. package/tests/client/capture-error.js +12 -0
  21. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  22. package/tests/client/composite/composite.props.test.ripple +1 -3
  23. package/tests/client/composite/composite.render.test.ripple +45 -13
  24. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  25. package/tests/client/svg.test.ripple +4 -4
  26. package/tests/hydration/basic.test.js +23 -0
  27. package/tests/hydration/compiled/client/basic.js +118 -66
  28. package/tests/hydration/compiled/client/composite.js +90 -37
  29. package/tests/hydration/compiled/client/events.js +18 -18
  30. package/tests/hydration/compiled/client/for.js +62 -62
  31. package/tests/hydration/compiled/client/head.js +10 -10
  32. package/tests/hydration/compiled/client/hmr.js +13 -10
  33. package/tests/hydration/compiled/client/html.js +274 -236
  34. package/tests/hydration/compiled/client/if-children.js +41 -35
  35. package/tests/hydration/compiled/client/if.js +2 -2
  36. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  37. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  38. package/tests/hydration/compiled/client/portal.js +8 -8
  39. package/tests/hydration/compiled/client/reactivity.js +14 -14
  40. package/tests/hydration/compiled/client/return.js +2 -2
  41. package/tests/hydration/compiled/client/try.js +4 -4
  42. package/tests/hydration/compiled/server/basic.js +64 -31
  43. package/tests/hydration/compiled/server/composite.js +62 -29
  44. package/tests/hydration/compiled/server/hmr.js +24 -37
  45. package/tests/hydration/compiled/server/html.js +472 -611
  46. package/tests/hydration/compiled/server/if-children.js +77 -103
  47. package/tests/hydration/compiled/server/portal.js +8 -8
  48. package/tests/hydration/components/basic.ripple +15 -5
  49. package/tests/hydration/components/composite.ripple +13 -1
  50. package/tests/hydration/components/hmr.ripple +1 -3
  51. package/tests/hydration/components/html.ripple +13 -35
  52. package/tests/hydration/components/if-children.ripple +4 -8
  53. package/tests/hydration/composite.test.js +11 -0
  54. package/tests/server/basic.attributes.test.ripple +50 -0
  55. package/tests/server/basic.components.test.ripple +22 -28
  56. package/tests/server/basic.test.ripple +12 -0
  57. package/tests/server/compiler.test.ripple +25 -8
  58. package/tests/server/composite.props.test.ripple +1 -3
  59. package/tests/server/style-identifier.test.ripple +2 -4
  60. package/types/index.d.ts +9 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.10
4
+
5
+ ### Patch Changes
6
+
7
+ - [`aef1253`](https://github.com/Ripple-TS/ripple/commit/aef1253dd79c067a8358172d502dc21d8a9a9085)
8
+ Thanks [@trueadm](https://github.com/trueadm)! - Replace `<children />` with
9
+ `{children}` expression syntax for rendering component children
10
+
11
+ - Updated dependencies
12
+ [[`aef1253`](https://github.com/Ripple-TS/ripple/commit/aef1253dd79c067a8358172d502dc21d8a9a9085)]:
13
+ - ripple@0.3.10
14
+
3
15
  ## 0.3.9
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.9",
6
+ "version": "0.3.10",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -105,6 +105,6 @@
105
105
  "vscode-languageserver-types": "^3.17.5"
106
106
  },
107
107
  "peerDependencies": {
108
- "ripple": "0.3.9"
108
+ "ripple": "0.3.10"
109
109
  }
110
110
  }
@@ -1115,7 +1115,7 @@ function RipplePlugin(config) {
1115
1115
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1116
1116
  this.next();
1117
1117
 
1118
- if (this.value === 'html') {
1118
+ if (this.type === tt.name && this.value === 'html') {
1119
1119
  node.html = true;
1120
1120
  this.next();
1121
1121
  if (this.type === tt.braceR) {
@@ -1124,6 +1124,15 @@ function RipplePlugin(config) {
1124
1124
  '"html" is a Ripple keyword and must be used in the form {html some_content}',
1125
1125
  );
1126
1126
  }
1127
+ } else if (this.type === tt.name && this.value === 'text') {
1128
+ node.text = true;
1129
+ this.next();
1130
+ if (this.type === tt.braceR) {
1131
+ this.raise(
1132
+ this.start,
1133
+ '"text" is a Ripple keyword and must be used in the form {text some_value}',
1134
+ );
1135
+ }
1127
1136
  }
1128
1137
 
1129
1138
  node.expression =
@@ -1328,16 +1337,12 @@ function RipplePlugin(config) {
1328
1337
  const clause = /** @type {AST.CatchClause} */ (this.startNode());
1329
1338
  this.next();
1330
1339
  if (this.eat(tt.parenL)) {
1331
- clause.param = this.parseCatchClauseParam();
1340
+ clause.param = this.parseBindingAtom();
1341
+ this.expect(tt.parenR);
1332
1342
  } else {
1333
- if (this.options.ecmaVersion < 10) {
1334
- this.unexpected();
1335
- }
1336
1343
  clause.param = null;
1337
- this.enterScope(0);
1338
1344
  }
1339
- clause.body = this.parseBlock(false);
1340
- this.exitScope();
1345
+ clause.body = this.parseBlock();
1341
1346
  node.handler = this.finishNode(clause, 'CatchClause');
1342
1347
  }
1343
1348
  node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
@@ -1909,12 +1914,13 @@ function RipplePlugin(config) {
1909
1914
  if (this.type === tt.braceL) {
1910
1915
  const node = this.jsx_parseExpressionContainer();
1911
1916
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
1912
- // but convert other expressions to Text/Html nodes
1917
+ // but convert other expressions to Html/RippleExpression/Text nodes
1913
1918
  if (node.expression.type !== 'JSXEmptyExpression') {
1914
- /** @type {AST.Html | AST.TextNode} */ (/** @type {unknown} */ (node)).type = node.html
1915
- ? 'Html'
1916
- : 'Text';
1919
+ /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
1920
+ /** @type {unknown} */ (node)
1921
+ ).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
1917
1922
  delete node.html;
1923
+ delete node.text;
1918
1924
  }
1919
1925
  body.push(node);
1920
1926
  } else if (this.type === tt.braceR) {
@@ -2050,12 +2056,16 @@ function RipplePlugin(config) {
2050
2056
  this.context.some((c) => c === tstc.tc_expr)
2051
2057
  ) {
2052
2058
  const node = this.jsx_parseExpressionContainer();
2053
- // Keep JSXEmptyExpression as-is (don't convert to Text)
2059
+ // Keep JSXEmptyExpression as-is (don't convert to RippleExpression/Text/Html)
2054
2060
  if (node.expression.type !== 'JSXEmptyExpression') {
2055
- /** @type {AST.TextNode} */ (/** @type {unknown} */ (node)).type = 'Text';
2061
+ /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
2062
+ /** @type {unknown} */ (node)
2063
+ ).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
2064
+ delete node.html;
2065
+ delete node.text;
2056
2066
  }
2057
2067
 
2058
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2068
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.RippleExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2059
2069
  /** @type {unknown} */ (node)
2060
2070
  );
2061
2071
  }
@@ -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,
@@ -687,37 +688,6 @@ function error_return_keyword(node, context, message) {
687
688
  );
688
689
  }
689
690
 
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
691
  /**
722
692
  * @param {AST.Expression} expression
723
693
  * @param {Context<AST.Node, AnalysisState>} context
@@ -726,44 +696,7 @@ function unwrap_template_expression(expression) {
726
696
  function is_children_template_expression(expression, context) {
727
697
  const component = context.path.findLast((node) => node.type === 'Component');
728
698
  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
- );
699
+ return is_children_template_expression_in_scope(expression, context.state.scope, component_scope);
767
700
  }
768
701
 
769
702
  /** @type {Visitors<AST.Node, AnalysisState>} */
@@ -999,7 +932,7 @@ const visitors = {
999
932
  is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
1000
933
  ) {
1001
934
  error(
1002
- '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
935
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
1003
936
  context.state.analysis.module.filename,
1004
937
  callee,
1005
938
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -1718,6 +1651,19 @@ const visitors = {
1718
1651
 
1719
1652
  mark_control_flow_has_template(path);
1720
1653
 
1654
+ if (
1655
+ !is_dom_element &&
1656
+ is_children_template_expression(/** @type {AST.Expression} */ (node.id), context)
1657
+ ) {
1658
+ error(
1659
+ '`children` cannot be rendered as a component. Render it with `{children}` or `{props.children}` instead.',
1660
+ state.analysis.module.filename,
1661
+ node.id,
1662
+ context.state.loose ? context.state.analysis.errors : undefined,
1663
+ context.state.analysis.comments,
1664
+ );
1665
+ }
1666
+
1721
1667
  validate_nesting(node, context);
1722
1668
 
1723
1669
  // Store capitalized name for dynamic components/elements
@@ -1771,7 +1717,10 @@ const visitors = {
1771
1717
  if (/** @type {AST.Identifier} */ (node.id).name === 'title') {
1772
1718
  const children = normalize_children(node.children, context);
1773
1719
 
1774
- if (children.length !== 1 || children[0].type !== 'Text') {
1720
+ if (
1721
+ children.length !== 1 ||
1722
+ (children[0].type !== 'RippleExpression' && children[0].type !== 'Text')
1723
+ ) {
1775
1724
  // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1776
1725
  error(
1777
1726
  '<title> must have only contain text nodes',
@@ -1916,30 +1865,22 @@ const visitors = {
1916
1865
  }
1917
1866
  /** @type {(AST.Node | AST.Expression)[]} */
1918
1867
  let implicit_children = [];
1919
- /** @type {AST.Identifier[]} */
1920
- let explicit_children = [];
1921
1868
 
1922
1869
  for (const child of node.children) {
1923
1870
  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
1871
  error(
1937
- 'Cannot have both implicit and explicit children',
1872
+ 'Component declarations cannot be used inside composite component children. Pass them as explicit props on the template element instead.',
1938
1873
  state.analysis.module.filename,
1939
- item,
1874
+ child.id || child,
1940
1875
  context.state.loose ? context.state.analysis.errors : undefined,
1941
1876
  context.state.analysis.comments,
1942
1877
  );
1878
+ } else if (child.type !== 'EmptyStatement') {
1879
+ implicit_children.push(
1880
+ child.type === 'RippleExpression' || child.type === 'Text' || child.type === 'Html'
1881
+ ? child.expression
1882
+ : child,
1883
+ );
1943
1884
  }
1944
1885
  }
1945
1886
  }
@@ -1966,12 +1907,18 @@ const visitors = {
1966
1907
  };
1967
1908
  },
1968
1909
 
1910
+ RippleExpression(node, context) {
1911
+ mark_control_flow_has_template(context.path);
1912
+
1913
+ context.next();
1914
+ },
1915
+
1969
1916
  Text(node, context) {
1970
1917
  mark_control_flow_has_template(context.path);
1971
1918
 
1972
1919
  if (is_children_template_expression(/** @type {AST.Expression} */ (node.expression), context)) {
1973
1920
  error(
1974
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
1921
+ '`children` cannot be rendered using explicit text interpolation. Use `{children}` or `{props.children}` instead.',
1975
1922
  context.state.analysis.module.filename,
1976
1923
  node.expression,
1977
1924
  context.state.loose ? context.state.analysis.errors : undefined,
@@ -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()