@tsrx/react 0.0.7 → 0.1.1

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 +2 -2
  2. package/src/transform.js +76 -160
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.7",
6
+ "version": "0.1.1",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "esrap": "^2.1.0",
27
27
  "zimmerframe": "^1.1.2",
28
- "@tsrx/core": "0.0.5"
28
+ "@tsrx/core": "0.0.7"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": ">=18"
package/src/transform.js CHANGED
@@ -14,6 +14,10 @@ import {
14
14
  replaceLazyParams as replace_lazy_params,
15
15
  prepareStylesheetForRender as prepare_stylesheet_for_render,
16
16
  annotateComponentWithHash as annotate_component_with_hash,
17
+ isInterleavedBody as is_interleaved_body_core,
18
+ isCapturableJsxChild as is_capturable_jsx_child,
19
+ captureJsxChild,
20
+ isHoistSafeJsxNode as is_hoist_safe_jsx_node,
17
21
  } from '@tsrx/core';
18
22
 
19
23
  /**
@@ -51,7 +55,6 @@ import {
51
55
  export function transform(ast, source, filename) {
52
56
  /** @type {any[]} */
53
57
  const stylesheets = [];
54
- const module_uses_server_directive = has_use_server_directive(ast);
55
58
 
56
59
  /** @type {TransformContext} */
57
60
  const transform_context = {
@@ -71,13 +74,6 @@ export function transform(ast, source, filename) {
71
74
  const as_any = /** @type {any} */ (node);
72
75
  const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
73
76
 
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
77
  if (await_expression) {
82
78
  as_any.metadata = /** @type {any} */ ({
83
79
  ...(as_any.metadata || {}),
@@ -304,6 +300,10 @@ function build_component_statements(
304
300
  const render_nodes = [];
305
301
  const bindings = new Map(available_bindings);
306
302
 
303
+ const pre_split_body = body_nodes.slice(0, split_index);
304
+ const interleaved = is_interleaved_body(pre_split_body);
305
+ let capture_index = 0;
306
+
307
307
  for (let i = 0; i < split_index; i += 1) {
308
308
  const child = body_nodes[i];
309
309
 
@@ -318,7 +318,14 @@ function build_component_statements(
318
318
  }
319
319
 
320
320
  if (is_jsx_child(child)) {
321
- render_nodes.push(to_jsx_child(child, transform_context));
321
+ const jsx = to_jsx_child(child, transform_context);
322
+ if (interleaved && is_capturable_jsx_child(jsx)) {
323
+ const { declaration, reference } = captureJsxChild(jsx, capture_index++);
324
+ statements.push(declaration);
325
+ render_nodes.push(reference);
326
+ } else {
327
+ render_nodes.push(jsx);
328
+ }
322
329
  } else {
323
330
  statements.push(child);
324
331
  collect_statement_bindings(child, bindings);
@@ -326,7 +333,9 @@ function build_component_statements(
326
333
  }
327
334
  }
328
335
 
329
- hoist_static_render_nodes(render_nodes, transform_context);
336
+ if (!interleaved) {
337
+ hoist_static_render_nodes(render_nodes, transform_context);
338
+ }
330
339
 
331
340
  const split_node = body_nodes[split_index];
332
341
  const consequent_body =
@@ -389,6 +398,14 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
389
398
  const saved_bindings = transform_context.available_bindings;
390
399
  transform_context.available_bindings = new Map(saved_bindings);
391
400
 
401
+ // When non-JSX statements are interleaved with JSX children, we must
402
+ // preserve source order so each JSX expression sees the variable state
403
+ // at its textual position. Otherwise statements would all run before
404
+ // any JSX is constructed, and every JSX child would observe the final
405
+ // state of mutable variables.
406
+ const interleaved = is_interleaved_body(body_nodes);
407
+ let capture_index = 0;
408
+
392
409
  for (const child of body_nodes) {
393
410
  if (is_bare_return_statement(child)) {
394
411
  statements.push(create_component_return_statement(render_nodes, child));
@@ -402,14 +419,23 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
402
419
  }
403
420
 
404
421
  if (is_jsx_child(child)) {
405
- render_nodes.push(to_jsx_child(child, transform_context));
422
+ const jsx = to_jsx_child(child, transform_context);
423
+ if (interleaved && is_capturable_jsx_child(jsx)) {
424
+ const { declaration, reference } = captureJsxChild(jsx, capture_index++);
425
+ statements.push(declaration);
426
+ render_nodes.push(reference);
427
+ } else {
428
+ render_nodes.push(jsx);
429
+ }
406
430
  } else {
407
431
  statements.push(child);
408
432
  collect_statement_bindings(child, transform_context.available_bindings);
409
433
  }
410
434
  }
411
435
 
412
- hoist_static_render_nodes(render_nodes, transform_context);
436
+ if (!interleaved) {
437
+ hoist_static_render_nodes(render_nodes, transform_context);
438
+ }
413
439
 
414
440
  const return_arg = build_return_expression(render_nodes);
415
441
  if (return_arg || return_null_when_empty) {
@@ -423,6 +449,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
423
449
  return statements;
424
450
  }
425
451
 
452
+ /**
453
+ * React-specific wrapper around the core `isInterleavedBody` helper that
454
+ * ignores bare `return` / lone return-if statements. Those are rewriting
455
+ * signals rather than user-visible side effects, so JSX children around
456
+ * them don't need capturing.
457
+ *
458
+ * @param {any[]} body_nodes
459
+ * @returns {boolean}
460
+ */
461
+ function is_interleaved_body(body_nodes) {
462
+ const filtered = body_nodes.filter(
463
+ (child) => !is_bare_return_statement(child) && !is_lone_return_if_statement(child),
464
+ );
465
+ return is_interleaved_body_core(filtered, is_jsx_child);
466
+ }
467
+
426
468
  /**
427
469
  * @param {any[]} body_nodes
428
470
  * @returns {number}
@@ -535,34 +577,6 @@ function is_hook_callee(callee) {
535
577
  return false;
536
578
  }
537
579
 
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
-
566
580
  /**
567
581
  * @param {any[]} body_nodes
568
582
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
@@ -881,116 +895,6 @@ function references_scope_bindings(node, scope_bindings) {
881
895
  return false;
882
896
  }
883
897
 
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
-
994
898
  /**
995
899
  * Hoist static JSX elements from render_nodes to module level.
996
900
  * A JSX element is static if it doesn't reference any component-scope bindings.
@@ -1203,6 +1107,19 @@ function to_jsx_element(node, transform_context) {
1203
1107
  return dynamic_element_to_jsx_child(node, transform_context);
1204
1108
  }
1205
1109
 
1110
+ if (!node.id) {
1111
+ const children = create_element_children(node.children || [], transform_context);
1112
+ return set_loc(
1113
+ /** @type {any} */ ({
1114
+ type: 'JSXFragment',
1115
+ openingFragment: { type: 'JSXOpeningFragment' },
1116
+ closingFragment: { type: 'JSXClosingFragment' },
1117
+ children,
1118
+ }),
1119
+ node,
1120
+ );
1121
+ }
1122
+
1206
1123
  const name = identifier_to_jsx_name(node.id);
1207
1124
  const attributes = (node.attributes || []).map(to_jsx_attribute);
1208
1125
  const selfClosing = !!node.selfClosing;
@@ -1686,24 +1603,23 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1686
1603
  );
1687
1604
  }
1688
1605
 
1689
- if (node.key) {
1690
- throw create_compile_error(
1691
- node.key,
1692
- 'React TSRX does not support `key` in `for` control flow. Put the key on the rendered element instead, for example `<div key={i}>...</div>`.',
1693
- );
1694
- }
1695
-
1696
1606
  const loop_params = get_for_of_iteration_params(node.left, node.index);
1697
1607
  const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
1698
1608
  const has_hooks = body_contains_top_level_hook_call(loop_body);
1699
- const explicit_key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1609
+ const body_key_expression = find_key_expression_in_body(loop_body);
1610
+ const explicit_key_expression =
1611
+ body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
1700
1612
  const key_expression =
1701
1613
  has_hooks && explicit_key_expression == null && node.index
1702
1614
  ? clone_expression_node(node.index)
1703
1615
  : explicit_key_expression;
1704
1616
  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)
1617
+ !has_hooks && body_key_expression == null
1618
+ ? node.key
1619
+ ? clone_expression_node(node.key)
1620
+ : node.index
1621
+ ? clone_expression_node(node.index)
1622
+ : undefined
1707
1623
  : undefined;
1708
1624
 
1709
1625
  // Add loop params to available bindings so hoisted helpers receive them as props