@tsrx/react 0.0.5 → 0.0.7

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/transform.js +221 -2
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "React compiler built on @tsrx/core",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.5",
6
+ "version": "0.0.7",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
package/src/transform.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  renderStylesheets,
9
9
  setLocation,
10
10
  applyLazyTransforms as apply_lazy_transforms,
11
+ findFirstTopLevelAwaitInComponentBody as find_first_top_level_await_in_component_body,
11
12
  collectLazyBindingsFromComponent as collect_lazy_bindings_from_component,
12
13
  preallocateLazyIds as preallocate_lazy_ids,
13
14
  replaceLazyParams as replace_lazy_params,
@@ -50,6 +51,7 @@ import {
50
51
  export function transform(ast, source, filename) {
51
52
  /** @type {any[]} */
52
53
  const stylesheets = [];
54
+ const module_uses_server_directive = has_use_server_directive(ast);
53
55
 
54
56
  /** @type {TransformContext} */
55
57
  const transform_context = {
@@ -67,6 +69,22 @@ export function transform(ast, source, filename) {
67
69
  walk(/** @type {any} */ (ast), transform_context, {
68
70
  Component(node, { next, state }) {
69
71
  const as_any = /** @type {any} */ (node);
72
+ const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
73
+
74
+ if (await_expression && !module_uses_server_directive) {
75
+ throw create_compile_error(
76
+ await_expression,
77
+ 'React components can only use `await` when the module has a top-level "use server" directive.',
78
+ );
79
+ }
80
+
81
+ if (await_expression) {
82
+ as_any.metadata = /** @type {any} */ ({
83
+ ...(as_any.metadata || {}),
84
+ contains_top_level_await: true,
85
+ });
86
+ }
87
+
70
88
  const css = as_any.css;
71
89
  if (css) {
72
90
  stylesheets.push(css);
@@ -194,6 +212,9 @@ function component_to_function_declaration(component, transform_context, walk_he
194
212
  const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
195
213
  const params = component.params || [];
196
214
  const body = /** @type {any[]} */ (component.body || []);
215
+ const is_async_component =
216
+ !!component?.metadata?.contains_top_level_await ||
217
+ find_first_top_level_await_in_component_body(body) !== null;
197
218
 
198
219
  // Collect param bindings from original patterns (lazy patterns still intact).
199
220
  const param_bindings = collect_param_bindings(params);
@@ -235,7 +256,7 @@ function component_to_function_declaration(component, transform_context, walk_he
235
256
  id: component.id,
236
257
  params: final_params,
237
258
  body: final_body,
238
- async: false,
259
+ async: is_async_component,
239
260
  generator: false,
240
261
  metadata: {
241
262
  path: [],
@@ -514,6 +535,34 @@ function is_hook_callee(callee) {
514
535
  return false;
515
536
  }
516
537
 
538
+ /**
539
+ * @param {AST.Program} program
540
+ * @returns {boolean}
541
+ */
542
+ function has_use_server_directive(program) {
543
+ for (const statement of program.body || []) {
544
+ const directive = /** @type {any} */ (statement).directive;
545
+
546
+ if (directive === 'use server') {
547
+ return true;
548
+ }
549
+
550
+ if (
551
+ statement.type === 'ExpressionStatement' &&
552
+ statement.expression?.type === 'Literal' &&
553
+ statement.expression.value === 'use server'
554
+ ) {
555
+ return true;
556
+ }
557
+
558
+ if (directive == null) {
559
+ break;
560
+ }
561
+ }
562
+
563
+ return false;
564
+ }
565
+
517
566
  /**
518
567
  * @param {any[]} body_nodes
519
568
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
@@ -832,6 +881,116 @@ function references_scope_bindings(node, scope_bindings) {
832
881
  return false;
833
882
  }
834
883
 
884
+ /**
885
+ * @param {AST.Literal} node
886
+ * @returns {boolean}
887
+ */
888
+ function is_static_literal(node) {
889
+ return (
890
+ node.value === null ||
891
+ typeof node.value === 'string' ||
892
+ typeof node.value === 'number' ||
893
+ typeof node.value === 'boolean' ||
894
+ typeof node.value === 'bigint'
895
+ );
896
+ }
897
+
898
+ /**
899
+ * @param {any} node
900
+ * @returns {boolean}
901
+ */
902
+ function is_hoist_safe_expression(node) {
903
+ if (!node || typeof node !== 'object') return false;
904
+
905
+ switch (node.type) {
906
+ case 'Literal':
907
+ return is_static_literal(node);
908
+ case 'TemplateLiteral':
909
+ return node.expressions.length === 0;
910
+ case 'UnaryExpression':
911
+ return node.operator !== 'delete' && is_hoist_safe_expression(node.argument);
912
+ case 'BinaryExpression':
913
+ case 'LogicalExpression':
914
+ return is_hoist_safe_expression(node.left) && is_hoist_safe_expression(node.right);
915
+ case 'ConditionalExpression':
916
+ return (
917
+ is_hoist_safe_expression(node.test) &&
918
+ is_hoist_safe_expression(node.consequent) &&
919
+ is_hoist_safe_expression(node.alternate)
920
+ );
921
+ case 'SequenceExpression':
922
+ return node.expressions.every(is_hoist_safe_expression);
923
+ case 'ParenthesizedExpression':
924
+ return is_hoist_safe_expression(node.expression);
925
+ case 'JSXElement':
926
+ return is_hoist_safe_jsx_node(node);
927
+ case 'JSXFragment':
928
+ return node.children.every(is_hoist_safe_jsx_child);
929
+ default:
930
+ return false;
931
+ }
932
+ }
933
+
934
+ /**
935
+ * @param {any} node
936
+ * @returns {boolean}
937
+ */
938
+ function is_hoist_safe_jsx_child(node) {
939
+ if (!node || typeof node !== 'object') return false;
940
+
941
+ switch (node.type) {
942
+ case 'JSXText':
943
+ return true;
944
+ case 'JSXElement':
945
+ return is_hoist_safe_jsx_node(node);
946
+ case 'JSXFragment':
947
+ return node.children.every(is_hoist_safe_jsx_child);
948
+ case 'JSXExpressionContainer':
949
+ return (
950
+ node.expression.type !== 'JSXEmptyExpression' && is_hoist_safe_expression(node.expression)
951
+ );
952
+ default:
953
+ return false;
954
+ }
955
+ }
956
+
957
+ /**
958
+ * @param {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} attribute
959
+ * @returns {boolean}
960
+ */
961
+ function is_hoist_safe_jsx_attribute(attribute) {
962
+ if (attribute.type === 'JSXSpreadAttribute') return false;
963
+ if (attribute.value == null) return true;
964
+
965
+ if (attribute.value.type === 'Literal') {
966
+ return is_static_literal(attribute.value);
967
+ }
968
+
969
+ if (attribute.value.type === 'JSXExpressionContainer') {
970
+ return (
971
+ attribute.value.expression.type !== 'JSXEmptyExpression' &&
972
+ is_hoist_safe_expression(attribute.value.expression)
973
+ );
974
+ }
975
+
976
+ return false;
977
+ }
978
+
979
+ /**
980
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
981
+ * @returns {boolean}
982
+ */
983
+ function is_hoist_safe_jsx_node(node) {
984
+ if (node.type === 'JSXFragment') {
985
+ return node.children.every(is_hoist_safe_jsx_child);
986
+ }
987
+
988
+ return (
989
+ node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
990
+ node.children.every(is_hoist_safe_jsx_child)
991
+ );
992
+ }
993
+
835
994
  /**
836
995
  * Hoist static JSX elements from render_nodes to module level.
837
996
  * A JSX element is static if it doesn't reference any component-scope bindings.
@@ -847,6 +1006,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
847
1006
  for (let i = 0; i < render_nodes.length; i++) {
848
1007
  const node = render_nodes[i];
849
1008
  if (node.type !== 'JSXElement') continue;
1009
+ if (!is_hoist_safe_jsx_node(node)) continue;
850
1010
  if (references_scope_bindings(node, transform_context.available_bindings)) continue;
851
1011
 
852
1012
  const name = create_helper_name(transform_context.helper_state, 'static');
@@ -1519,6 +1679,13 @@ function find_key_expression_in_body(body_nodes) {
1519
1679
  * @returns {ESTreeJSX.JSXExpressionContainer}
1520
1680
  */
1521
1681
  function for_of_statement_to_jsx_child(node, transform_context) {
1682
+ if (node.await) {
1683
+ throw create_compile_error(
1684
+ node,
1685
+ 'React TSRX does not support `for await...of` in component templates.',
1686
+ );
1687
+ }
1688
+
1522
1689
  if (node.key) {
1523
1690
  throw create_compile_error(
1524
1691
  node.key,
@@ -1529,7 +1696,15 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1529
1696
  const loop_params = get_for_of_iteration_params(node.left, node.index);
1530
1697
  const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
1531
1698
  const has_hooks = body_contains_top_level_hook_call(loop_body);
1532
- const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1699
+ const explicit_key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1700
+ const key_expression =
1701
+ has_hooks && explicit_key_expression == null && node.index
1702
+ ? clone_expression_node(node.index)
1703
+ : explicit_key_expression;
1704
+ const implicit_non_hook_key_expression =
1705
+ !has_hooks && node.index && find_key_expression_in_body(loop_body) == null
1706
+ ? clone_expression_node(node.index)
1707
+ : undefined;
1533
1708
 
1534
1709
  // Add loop params to available bindings so hoisted helpers receive them as props
1535
1710
  const saved_bindings = transform_context.available_bindings;
@@ -1542,6 +1717,10 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1542
1717
  ? hook_safe_render_statements(loop_body, key_expression, transform_context)
1543
1718
  : build_render_statements(loop_body, true, transform_context);
1544
1719
 
1720
+ if (implicit_non_hook_key_expression) {
1721
+ apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
1722
+ }
1723
+
1545
1724
  // Restore bindings
1546
1725
  transform_context.available_bindings = saved_bindings;
1547
1726
 
@@ -1578,6 +1757,46 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1578
1757
  );
1579
1758
  }
1580
1759
 
1760
+ /**
1761
+ * @param {any[]} statements
1762
+ * @param {any} key_expression
1763
+ * @returns {void}
1764
+ */
1765
+ function apply_key_to_render_statements(statements, key_expression) {
1766
+ for (let i = statements.length - 1; i >= 0; i -= 1) {
1767
+ const statement = statements[i];
1768
+ if (statement?.type !== 'ReturnStatement' || !statement.argument) {
1769
+ continue;
1770
+ }
1771
+
1772
+ if (statement.argument.type === 'JSXElement') {
1773
+ const attributes = statement.argument.openingElement?.attributes || [];
1774
+ const has_key = attributes.some(
1775
+ (/** @type {any} */ attr) =>
1776
+ attr.type === 'JSXAttribute' &&
1777
+ attr.name?.type === 'JSXIdentifier' &&
1778
+ attr.name.name === 'key',
1779
+ );
1780
+
1781
+ if (!has_key) {
1782
+ attributes.push(
1783
+ /** @type {any} */ ({
1784
+ type: 'JSXAttribute',
1785
+ name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
1786
+ value: to_jsx_expression_container(
1787
+ clone_expression_node(key_expression),
1788
+ key_expression,
1789
+ ),
1790
+ metadata: { path: [] },
1791
+ }),
1792
+ );
1793
+ }
1794
+ }
1795
+
1796
+ return;
1797
+ }
1798
+ }
1799
+
1581
1800
  /**
1582
1801
  * @param {any} node
1583
1802
  * @param {TransformContext} transform_context