@tsrx/react 0.0.6 → 0.1.0

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 +177 -40
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.6",
6
+ "version": "0.1.0",
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.6"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": ">=18"
package/src/transform.js CHANGED
@@ -14,6 +14,9 @@ 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,
17
20
  } from '@tsrx/core';
18
21
 
19
22
  /**
@@ -51,7 +54,6 @@ import {
51
54
  export function transform(ast, source, filename) {
52
55
  /** @type {any[]} */
53
56
  const stylesheets = [];
54
- const module_uses_server_directive = has_use_server_directive(ast);
55
57
 
56
58
  /** @type {TransformContext} */
57
59
  const transform_context = {
@@ -71,13 +73,6 @@ export function transform(ast, source, filename) {
71
73
  const as_any = /** @type {any} */ (node);
72
74
  const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
73
75
 
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
76
  if (await_expression) {
82
77
  as_any.metadata = /** @type {any} */ ({
83
78
  ...(as_any.metadata || {}),
@@ -304,6 +299,10 @@ function build_component_statements(
304
299
  const render_nodes = [];
305
300
  const bindings = new Map(available_bindings);
306
301
 
302
+ const pre_split_body = body_nodes.slice(0, split_index);
303
+ const interleaved = is_interleaved_body(pre_split_body);
304
+ let capture_index = 0;
305
+
307
306
  for (let i = 0; i < split_index; i += 1) {
308
307
  const child = body_nodes[i];
309
308
 
@@ -318,7 +317,14 @@ function build_component_statements(
318
317
  }
319
318
 
320
319
  if (is_jsx_child(child)) {
321
- render_nodes.push(to_jsx_child(child, transform_context));
320
+ const jsx = to_jsx_child(child, transform_context);
321
+ if (interleaved && is_capturable_jsx_child(jsx)) {
322
+ const { declaration, reference } = captureJsxChild(jsx, capture_index++);
323
+ statements.push(declaration);
324
+ render_nodes.push(reference);
325
+ } else {
326
+ render_nodes.push(jsx);
327
+ }
322
328
  } else {
323
329
  statements.push(child);
324
330
  collect_statement_bindings(child, bindings);
@@ -326,7 +332,9 @@ function build_component_statements(
326
332
  }
327
333
  }
328
334
 
329
- hoist_static_render_nodes(render_nodes, transform_context);
335
+ if (!interleaved) {
336
+ hoist_static_render_nodes(render_nodes, transform_context);
337
+ }
330
338
 
331
339
  const split_node = body_nodes[split_index];
332
340
  const consequent_body =
@@ -389,6 +397,14 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
389
397
  const saved_bindings = transform_context.available_bindings;
390
398
  transform_context.available_bindings = new Map(saved_bindings);
391
399
 
400
+ // When non-JSX statements are interleaved with JSX children, we must
401
+ // preserve source order so each JSX expression sees the variable state
402
+ // at its textual position. Otherwise statements would all run before
403
+ // any JSX is constructed, and every JSX child would observe the final
404
+ // state of mutable variables.
405
+ const interleaved = is_interleaved_body(body_nodes);
406
+ let capture_index = 0;
407
+
392
408
  for (const child of body_nodes) {
393
409
  if (is_bare_return_statement(child)) {
394
410
  statements.push(create_component_return_statement(render_nodes, child));
@@ -402,14 +418,23 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
402
418
  }
403
419
 
404
420
  if (is_jsx_child(child)) {
405
- render_nodes.push(to_jsx_child(child, transform_context));
421
+ const jsx = to_jsx_child(child, transform_context);
422
+ if (interleaved && is_capturable_jsx_child(jsx)) {
423
+ const { declaration, reference } = captureJsxChild(jsx, capture_index++);
424
+ statements.push(declaration);
425
+ render_nodes.push(reference);
426
+ } else {
427
+ render_nodes.push(jsx);
428
+ }
406
429
  } else {
407
430
  statements.push(child);
408
431
  collect_statement_bindings(child, transform_context.available_bindings);
409
432
  }
410
433
  }
411
434
 
412
- hoist_static_render_nodes(render_nodes, transform_context);
435
+ if (!interleaved) {
436
+ hoist_static_render_nodes(render_nodes, transform_context);
437
+ }
413
438
 
414
439
  const return_arg = build_return_expression(render_nodes);
415
440
  if (return_arg || return_null_when_empty) {
@@ -423,6 +448,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
423
448
  return statements;
424
449
  }
425
450
 
451
+ /**
452
+ * React-specific wrapper around the core `isInterleavedBody` helper that
453
+ * ignores bare `return` / lone return-if statements. Those are rewriting
454
+ * signals rather than user-visible side effects, so JSX children around
455
+ * them don't need capturing.
456
+ *
457
+ * @param {any[]} body_nodes
458
+ * @returns {boolean}
459
+ */
460
+ function is_interleaved_body(body_nodes) {
461
+ const filtered = body_nodes.filter(
462
+ (child) => !is_bare_return_statement(child) && !is_lone_return_if_statement(child),
463
+ );
464
+ return is_interleaved_body_core(filtered, is_jsx_child);
465
+ }
466
+
426
467
  /**
427
468
  * @param {any[]} body_nodes
428
469
  * @returns {number}
@@ -535,34 +576,6 @@ function is_hook_callee(callee) {
535
576
  return false;
536
577
  }
537
578
 
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
579
  /**
567
580
  * @param {any[]} body_nodes
568
581
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
@@ -881,6 +894,116 @@ function references_scope_bindings(node, scope_bindings) {
881
894
  return false;
882
895
  }
883
896
 
897
+ /**
898
+ * @param {AST.Literal} node
899
+ * @returns {boolean}
900
+ */
901
+ function is_static_literal(node) {
902
+ return (
903
+ node.value === null ||
904
+ typeof node.value === 'string' ||
905
+ typeof node.value === 'number' ||
906
+ typeof node.value === 'boolean' ||
907
+ typeof node.value === 'bigint'
908
+ );
909
+ }
910
+
911
+ /**
912
+ * @param {any} node
913
+ * @returns {boolean}
914
+ */
915
+ function is_hoist_safe_expression(node) {
916
+ if (!node || typeof node !== 'object') return false;
917
+
918
+ switch (node.type) {
919
+ case 'Literal':
920
+ return is_static_literal(node);
921
+ case 'TemplateLiteral':
922
+ return node.expressions.length === 0;
923
+ case 'UnaryExpression':
924
+ return node.operator !== 'delete' && is_hoist_safe_expression(node.argument);
925
+ case 'BinaryExpression':
926
+ case 'LogicalExpression':
927
+ return is_hoist_safe_expression(node.left) && is_hoist_safe_expression(node.right);
928
+ case 'ConditionalExpression':
929
+ return (
930
+ is_hoist_safe_expression(node.test) &&
931
+ is_hoist_safe_expression(node.consequent) &&
932
+ is_hoist_safe_expression(node.alternate)
933
+ );
934
+ case 'SequenceExpression':
935
+ return node.expressions.every(is_hoist_safe_expression);
936
+ case 'ParenthesizedExpression':
937
+ return is_hoist_safe_expression(node.expression);
938
+ case 'JSXElement':
939
+ return is_hoist_safe_jsx_node(node);
940
+ case 'JSXFragment':
941
+ return node.children.every(is_hoist_safe_jsx_child);
942
+ default:
943
+ return false;
944
+ }
945
+ }
946
+
947
+ /**
948
+ * @param {any} node
949
+ * @returns {boolean}
950
+ */
951
+ function is_hoist_safe_jsx_child(node) {
952
+ if (!node || typeof node !== 'object') return false;
953
+
954
+ switch (node.type) {
955
+ case 'JSXText':
956
+ return true;
957
+ case 'JSXElement':
958
+ return is_hoist_safe_jsx_node(node);
959
+ case 'JSXFragment':
960
+ return node.children.every(is_hoist_safe_jsx_child);
961
+ case 'JSXExpressionContainer':
962
+ return (
963
+ node.expression.type !== 'JSXEmptyExpression' && is_hoist_safe_expression(node.expression)
964
+ );
965
+ default:
966
+ return false;
967
+ }
968
+ }
969
+
970
+ /**
971
+ * @param {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} attribute
972
+ * @returns {boolean}
973
+ */
974
+ function is_hoist_safe_jsx_attribute(attribute) {
975
+ if (attribute.type === 'JSXSpreadAttribute') return false;
976
+ if (attribute.value == null) return true;
977
+
978
+ if (attribute.value.type === 'Literal') {
979
+ return is_static_literal(attribute.value);
980
+ }
981
+
982
+ if (attribute.value.type === 'JSXExpressionContainer') {
983
+ return (
984
+ attribute.value.expression.type !== 'JSXEmptyExpression' &&
985
+ is_hoist_safe_expression(attribute.value.expression)
986
+ );
987
+ }
988
+
989
+ return false;
990
+ }
991
+
992
+ /**
993
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
994
+ * @returns {boolean}
995
+ */
996
+ function is_hoist_safe_jsx_node(node) {
997
+ if (node.type === 'JSXFragment') {
998
+ return node.children.every(is_hoist_safe_jsx_child);
999
+ }
1000
+
1001
+ return (
1002
+ node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
1003
+ node.children.every(is_hoist_safe_jsx_child)
1004
+ );
1005
+ }
1006
+
884
1007
  /**
885
1008
  * Hoist static JSX elements from render_nodes to module level.
886
1009
  * A JSX element is static if it doesn't reference any component-scope bindings.
@@ -896,6 +1019,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
896
1019
  for (let i = 0; i < render_nodes.length; i++) {
897
1020
  const node = render_nodes[i];
898
1021
  if (node.type !== 'JSXElement') continue;
1022
+ if (!is_hoist_safe_jsx_node(node)) continue;
899
1023
  if (references_scope_bindings(node, transform_context.available_bindings)) continue;
900
1024
 
901
1025
  const name = create_helper_name(transform_context.helper_state, 'static');
@@ -1092,6 +1216,19 @@ function to_jsx_element(node, transform_context) {
1092
1216
  return dynamic_element_to_jsx_child(node, transform_context);
1093
1217
  }
1094
1218
 
1219
+ if (!node.id) {
1220
+ const children = create_element_children(node.children || [], transform_context);
1221
+ return set_loc(
1222
+ /** @type {any} */ ({
1223
+ type: 'JSXFragment',
1224
+ openingFragment: { type: 'JSXOpeningFragment' },
1225
+ closingFragment: { type: 'JSXClosingFragment' },
1226
+ children,
1227
+ }),
1228
+ node,
1229
+ );
1230
+ }
1231
+
1095
1232
  const name = identifier_to_jsx_name(node.id);
1096
1233
  const attributes = (node.attributes || []).map(to_jsx_attribute);
1097
1234
  const selfClosing = !!node.selfClosing;