@tsrx/core 0.0.12 → 0.0.14

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.
@@ -35,6 +35,8 @@ import {
35
35
  } from '../lazy.js';
36
36
  import { find_first_top_level_await_in_component_body } from '../await.js';
37
37
  import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
38
+ import { validate_component_return_statement } from '../../analyze/validation.js';
39
+ import { get_component_from_path } from '../../utils/ast.js';
38
40
  import {
39
41
  is_interleaved_body as is_interleaved_body_core,
40
42
  is_capturable_jsx_child,
@@ -48,7 +50,7 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
48
50
  * local_statement_component_index: number,
49
51
  * needs_error_boundary: boolean,
50
52
  * needs_suspense: boolean,
51
- * helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
53
+ * helper_state: { base_name: string, next_id: number, helpers: any[], statics: any[] } | null,
52
54
  * available_bindings: Map<string, AST.Identifier>,
53
55
  * lazy_next_id: number,
54
56
  * current_css_hash: string | null,
@@ -112,8 +114,17 @@ export function createJsxTransform(platform) {
112
114
  preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
113
115
 
114
116
  walk(/** @type {any} */ (ast), transform_context, {
117
+ ReturnStatement(node, { next, path }) {
118
+ if (get_component_from_path(path)) {
119
+ validate_component_return_statement(node, filename);
120
+ }
121
+
122
+ return next();
123
+ },
124
+
115
125
  Component(node, { next, state }) {
116
126
  const as_any = /** @type {any} */ (node);
127
+
117
128
  const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
118
129
 
119
130
  if (await_expression) {
@@ -170,17 +181,12 @@ export function createJsxTransform(platform) {
170
181
  state.current_css_hash = as_any.css ? as_any.css.hash : null;
171
182
 
172
183
  // Pre-collect component body bindings (params + top-level statements)
173
- // so that Element children processed during the bottom-up walk can see
174
- // the full scope. Without this, hoisted helpers would miss body-level
175
- // variables like `const [x] = useState(...)` and produce ReferenceErrors.
176
- // Only collect up to the split point — bindings declared after a
177
- // hook-safe split aren't in scope at the return statement and would
178
- // cause ReferenceErrors if passed as helper props.
184
+ // so Element children processed during the bottom-up walk can see
185
+ // component-scope names. Hook-safe helpers filter this set down to
186
+ // the names their body actually references before generating props.
179
187
  const body_bindings = collect_param_bindings(as_any.params || []);
180
188
  const body = as_any.body || [];
181
- const split_index = find_hook_safe_split_index(body);
182
- const collect_end = split_index === -1 ? body.length : split_index;
183
- for (let i = 0; i < collect_end; i += 1) {
189
+ for (let i = 0; i < body.length; i += 1) {
184
190
  collect_statement_bindings(body[i], body_bindings);
185
191
  }
186
192
  state.available_bindings = body_bindings;
@@ -347,12 +353,7 @@ function component_to_function_declaration(component, transform_context, walk_he
347
353
  transform_context.helper_state = helper_state;
348
354
  transform_context.available_bindings = new Map(param_bindings);
349
355
 
350
- const body_statements = build_component_statements(
351
- body,
352
- helper_state,
353
- param_bindings,
354
- transform_context,
355
- );
356
+ const body_statements = build_component_statements(body, transform_context);
356
357
 
357
358
  // Replace lazy param patterns with generated identifiers
358
359
  const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
@@ -371,6 +372,7 @@ function component_to_function_declaration(component, transform_context, walk_he
371
372
  const fn = /** @type {any} */ ({
372
373
  type: 'FunctionDeclaration',
373
374
  id: component.id,
375
+ typeParameters: component.typeParameters,
374
376
  params: final_params,
375
377
  body: final_body,
376
378
  async: is_async_component,
@@ -401,107 +403,11 @@ function component_to_function_declaration(component, transform_context, walk_he
401
403
 
402
404
  /**
403
405
  * @param {any[]} body_nodes
404
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
405
- * @param {Map<string, AST.Identifier>} available_bindings
406
406
  * @param {TransformContext} transform_context
407
407
  * @returns {any[]}
408
408
  */
409
- function build_component_statements(
410
- body_nodes,
411
- helper_state,
412
- available_bindings,
413
- transform_context,
414
- ) {
415
- const split_index = find_hook_safe_split_index(body_nodes);
416
- if (split_index === -1) {
417
- return build_render_statements(body_nodes, false, transform_context);
418
- }
419
-
420
- const statements = [];
421
- const render_nodes = [];
422
- const bindings = new Map(available_bindings);
423
-
424
- const pre_split_body = body_nodes.slice(0, split_index);
425
- const interleaved = is_interleaved_body(pre_split_body);
426
- let capture_index = 0;
427
-
428
- for (let i = 0; i < split_index; i += 1) {
429
- const child = body_nodes[i];
430
-
431
- if (is_bare_return_statement(child)) {
432
- statements.push(create_component_return_statement(render_nodes, child));
433
- return statements;
434
- }
435
-
436
- if (is_lone_return_if_statement(child)) {
437
- statements.push(create_component_lone_return_if_statement(child, render_nodes));
438
- continue;
439
- }
440
-
441
- if (is_jsx_child(child)) {
442
- const jsx = to_jsx_child(child, transform_context);
443
- if (interleaved && is_capturable_jsx_child(jsx)) {
444
- const { declaration, reference } = captureJsxChild(jsx, capture_index++);
445
- statements.push(declaration);
446
- render_nodes.push(reference);
447
- } else {
448
- render_nodes.push(jsx);
449
- }
450
- } else {
451
- statements.push(child);
452
- collect_statement_bindings(child, bindings);
453
- transform_context.available_bindings = bindings;
454
- }
455
- }
456
-
457
- if (!interleaved) {
458
- hoist_static_render_nodes(render_nodes, transform_context);
459
- }
460
-
461
- const split_node = body_nodes[split_index];
462
- const consequent_body =
463
- split_node.consequent.type === 'BlockStatement'
464
- ? split_node.consequent.body
465
- : [split_node.consequent];
466
- const short_branch_body = consequent_body.filter(
467
- (/** @type {any} */ child) => !is_bare_return_statement(child),
468
- );
469
- const continuation_body = body_nodes.slice(split_index + 1);
470
- const short_branch = create_helper_component_expression(
471
- short_branch_body,
472
- helper_state,
473
- bindings,
474
- split_node.consequent,
475
- 'Exit',
476
- transform_context,
477
- );
478
- const continuation = create_helper_component_expression(
479
- continuation_body,
480
- helper_state,
481
- bindings,
482
- split_node,
483
- 'Continue',
484
- transform_context,
485
- );
486
-
487
- render_nodes.push(
488
- to_jsx_expression_container(
489
- set_loc(
490
- /** @type {any} */ ({
491
- type: 'ConditionalExpression',
492
- test: split_node.test,
493
- consequent: short_branch,
494
- alternate: continuation,
495
- metadata: { path: [] },
496
- }),
497
- split_node,
498
- ),
499
- split_node,
500
- ),
501
- );
502
-
503
- statements.push(create_component_return_statement(render_nodes, split_node));
504
- return statements;
409
+ function build_component_statements(body_nodes, transform_context) {
410
+ return build_render_statements(body_nodes, false, transform_context);
505
411
  }
506
412
 
507
413
  /**
@@ -527,15 +433,40 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
527
433
  const interleaved = is_interleaved_body(body_nodes);
528
434
  let capture_index = 0;
529
435
 
530
- for (const child of body_nodes) {
436
+ for (let i = 0; i < body_nodes.length; i += 1) {
437
+ const child = body_nodes[i];
438
+
531
439
  if (is_bare_return_statement(child)) {
532
440
  statements.push(create_component_return_statement(render_nodes, child));
533
- transform_context.available_bindings = saved_bindings;
534
- return statements;
441
+ render_nodes.length = 0;
442
+ continue;
535
443
  }
536
444
 
537
- if (is_lone_return_if_statement(child)) {
538
- statements.push(create_component_lone_return_if_statement(child, render_nodes));
445
+ if (is_returning_if_statement(child)) {
446
+ const branch_has_hooks = body_contains_top_level_hook_call(get_if_consequent_body(child));
447
+ const continuation_has_hooks = body_contains_top_level_hook_call(body_nodes.slice(i + 1));
448
+
449
+ if (branch_has_hooks || continuation_has_hooks) {
450
+ statements.push(
451
+ ...create_component_helper_split_returning_if_statements(
452
+ child,
453
+ body_nodes.slice(i + 1),
454
+ render_nodes,
455
+ transform_context,
456
+ ),
457
+ );
458
+ transform_context.available_bindings = saved_bindings;
459
+ return statements;
460
+ }
461
+
462
+ if (is_lone_return_if_statement(child)) {
463
+ statements.push(create_component_lone_return_if_statement(child, render_nodes));
464
+ continue;
465
+ }
466
+
467
+ statements.push(
468
+ create_component_returning_if_statement(child, render_nodes, transform_context),
469
+ );
539
470
  continue;
540
471
  }
541
472
 
@@ -586,24 +517,6 @@ function is_interleaved_body(body_nodes) {
586
517
  return is_interleaved_body_core(filtered, is_jsx_child);
587
518
  }
588
519
 
589
- /**
590
- * @param {any[]} body_nodes
591
- * @returns {number}
592
- */
593
- function find_hook_safe_split_index(body_nodes) {
594
- for (let i = 0; i < body_nodes.length; i += 1) {
595
- if (!is_lone_return_if_statement(body_nodes[i])) {
596
- continue;
597
- }
598
-
599
- if (body_contains_top_level_hook_call(body_nodes.slice(i + 1))) {
600
- return i;
601
- }
602
- }
603
-
604
- return -1;
605
- }
606
-
607
520
  /**
608
521
  * @param {any[]} body_nodes
609
522
  * @returns {boolean}
@@ -698,96 +611,6 @@ function is_hook_callee(callee) {
698
611
  return false;
699
612
  }
700
613
 
701
- /**
702
- * @param {any[]} body_nodes
703
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
704
- * @param {Map<string, AST.Identifier>} available_bindings
705
- * @param {any} source_node
706
- * @param {string} suffix
707
- * @param {TransformContext} transform_context
708
- * @returns {any}
709
- */
710
- function create_helper_component_expression(
711
- body_nodes,
712
- helper_state,
713
- available_bindings,
714
- source_node,
715
- suffix,
716
- transform_context,
717
- ) {
718
- if (body_nodes.length === 0) {
719
- return create_null_literal();
720
- }
721
-
722
- const helper_name = create_helper_name(helper_state, suffix);
723
- const helper_id = set_loc(create_generated_identifier(helper_name), source_node);
724
- const helper_bindings = Array.from(available_bindings.values());
725
- const helper_fn = create_helper_function_declaration(
726
- helper_id,
727
- body_nodes,
728
- helper_state,
729
- available_bindings,
730
- helper_bindings,
731
- source_node,
732
- transform_context,
733
- );
734
-
735
- helper_state.helpers.push(helper_fn);
736
-
737
- return create_helper_component_element(helper_id, helper_bindings, source_node);
738
- }
739
-
740
- /**
741
- * @param {AST.Identifier} helper_id
742
- * @param {any[]} body_nodes
743
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
744
- * @param {Map<string, AST.Identifier>} available_bindings
745
- * @param {AST.Identifier[]} helper_bindings
746
- * @param {any} source_node
747
- * @param {TransformContext} transform_context
748
- * @returns {AST.FunctionDeclaration}
749
- */
750
- function create_helper_function_declaration(
751
- helper_id,
752
- body_nodes,
753
- helper_state,
754
- available_bindings,
755
- helper_bindings,
756
- source_node,
757
- transform_context,
758
- ) {
759
- const fn = /** @type {any} */ ({
760
- type: 'FunctionDeclaration',
761
- id: helper_id,
762
- params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
763
- body: {
764
- type: 'BlockStatement',
765
- body: build_component_statements(
766
- body_nodes,
767
- helper_state,
768
- new Map(available_bindings),
769
- transform_context,
770
- ),
771
- metadata: { path: [] },
772
- },
773
- async: false,
774
- generator: false,
775
- metadata: {
776
- path: [],
777
- is_component: true,
778
- },
779
- });
780
-
781
- if (fn.id) {
782
- fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
783
- ...fn.id.metadata,
784
- is_component: true,
785
- });
786
- }
787
-
788
- return set_loc(fn, source_node);
789
- }
790
-
791
614
  /**
792
615
  * @param {AST.Identifier[]} bindings
793
616
  * @returns {AST.ObjectPattern}
@@ -805,8 +628,8 @@ function create_helper_props_pattern(bindings) {
805
628
  * @returns {AST.Property}
806
629
  */
807
630
  function create_helper_props_property(binding) {
808
- const key = clone_identifier(binding);
809
- const value = clone_identifier(binding);
631
+ const key = create_generated_identifier(binding.name);
632
+ const value = create_generated_identifier(binding.name);
810
633
 
811
634
  return /** @type {any} */ ({
812
635
  type: 'Property',
@@ -824,42 +647,50 @@ function create_helper_props_property(binding) {
824
647
  * @param {AST.Identifier} helper_id
825
648
  * @param {AST.Identifier[]} bindings
826
649
  * @param {any} source_node
650
+ * @param {{
651
+ * mapWrapper?: boolean,
652
+ * mapBindingNames?: boolean,
653
+ * mapBindingValues?: boolean,
654
+ * }} [mapping]
827
655
  * @returns {ESTreeJSX.JSXElement}
828
656
  */
829
- function create_helper_component_element(helper_id, bindings, source_node) {
657
+ function create_helper_component_element(helper_id, bindings, source_node, mapping = {}) {
658
+ const { mapWrapper = true, mapBindingNames = true, mapBindingValues = true } = mapping;
830
659
  const attributes = bindings.map(
831
660
  (binding) =>
832
661
  /** @type {any} */ ({
833
662
  type: 'JSXAttribute',
834
- name: identifier_to_jsx_name(clone_identifier(binding)),
835
- value: to_jsx_expression_container(clone_identifier(binding), binding),
663
+ name: identifier_to_jsx_name(
664
+ mapBindingNames ? clone_identifier(binding) : create_generated_identifier(binding.name),
665
+ ),
666
+ value: to_jsx_expression_container(
667
+ mapBindingValues ? clone_identifier(binding) : create_generated_identifier(binding.name),
668
+ binding,
669
+ ),
836
670
  metadata: { path: [] },
837
671
  }),
838
672
  );
839
673
 
840
- return set_loc(
841
- /** @type {any} */ ({
842
- type: 'JSXElement',
843
- openingElement: set_loc(
844
- {
845
- type: 'JSXOpeningElement',
846
- name: identifier_to_jsx_name(clone_identifier(helper_id)),
847
- attributes,
848
- selfClosing: true,
849
- metadata: { path: [] },
850
- },
851
- source_node,
852
- ),
853
- closingElement: null,
854
- children: [],
855
- metadata: { path: [] },
856
- }),
857
- source_node,
858
- );
674
+ const openingElement = {
675
+ type: 'JSXOpeningElement',
676
+ name: identifier_to_jsx_name(clone_identifier(helper_id)),
677
+ attributes,
678
+ selfClosing: true,
679
+ metadata: { path: [] },
680
+ };
681
+ const element = /** @type {any} */ ({
682
+ type: 'JSXElement',
683
+ openingElement: mapWrapper ? set_loc(openingElement, source_node) : openingElement,
684
+ closingElement: null,
685
+ children: [],
686
+ metadata: { path: [] },
687
+ });
688
+
689
+ return mapWrapper ? set_loc(element, source_node) : element;
859
690
  }
860
691
 
861
692
  /**
862
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
693
+ * @param {{ base_name: string, next_id: number, helpers: any[], statics: any[] }} helper_state
863
694
  * @param {string} suffix
864
695
  * @returns {string}
865
696
  */
@@ -870,7 +701,7 @@ function create_helper_name(helper_state, suffix) {
870
701
 
871
702
  /**
872
703
  * @param {string} base_name
873
- * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
704
+ * @returns {{ base_name: string, next_id: number, helpers: any[], statics: any[] }}
874
705
  */
875
706
  function create_helper_state(base_name) {
876
707
  return {
@@ -1108,28 +939,61 @@ function is_lone_return_if_statement(node) {
1108
939
  return false;
1109
940
  }
1110
941
 
1111
- const consequent_body =
1112
- node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
942
+ const consequent_body = get_if_consequent_body(node);
1113
943
 
1114
944
  return consequent_body.length === 1 && is_bare_return_statement(consequent_body[0]);
1115
945
  }
1116
946
 
947
+ /**
948
+ * @param {any} node
949
+ * @returns {boolean}
950
+ */
951
+ function is_returning_if_statement(node) {
952
+ if (node?.type !== 'IfStatement' || node.alternate) {
953
+ return false;
954
+ }
955
+
956
+ return get_if_consequent_body(node).some(is_bare_return_statement);
957
+ }
958
+
959
+ /**
960
+ * @param {any} node
961
+ * @returns {any[]}
962
+ */
963
+ function get_if_consequent_body(node) {
964
+ return node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
965
+ }
966
+
1117
967
  /**
1118
968
  * @param {any[]} render_nodes
1119
969
  * @param {any} source_node
970
+ * @param {boolean} [map_render_node_locations]
1120
971
  * @returns {any}
1121
972
  */
1122
- function create_component_return_statement(render_nodes, source_node) {
1123
- return /** @type {any} */ ({
1124
- type: 'ReturnStatement',
1125
- argument: build_return_expression(render_nodes.slice()) || {
1126
- type: 'Literal',
1127
- value: null,
1128
- raw: 'null',
973
+ function create_component_return_statement(
974
+ render_nodes,
975
+ source_node,
976
+ map_render_node_locations = true,
977
+ ) {
978
+ return set_loc(
979
+ /** @type {any} */ ({
980
+ type: 'ReturnStatement',
981
+ argument: build_return_expression(
982
+ render_nodes.map((node) =>
983
+ map_render_node_locations
984
+ ? clone_expression_node(node)
985
+ : clone_expression_node_without_locations(node),
986
+ ),
987
+ ) || {
988
+ type: 'Literal',
989
+ value: null,
990
+ raw: 'null',
991
+ metadata: { path: [] },
992
+ },
1129
993
  metadata: { path: [] },
1130
- },
1131
- metadata: { path: [] },
1132
- });
994
+ }),
995
+ source_node,
996
+ );
1133
997
  }
1134
998
 
1135
999
  /**
@@ -1138,8 +1002,7 @@ function create_component_return_statement(render_nodes, source_node) {
1138
1002
  * @returns {any}
1139
1003
  */
1140
1004
  function create_component_lone_return_if_statement(node, render_nodes) {
1141
- const consequent_body =
1142
- node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
1005
+ const consequent_body = get_if_consequent_body(node);
1143
1006
 
1144
1007
  return set_loc(
1145
1008
  /** @type {any} */ ({
@@ -1148,7 +1011,7 @@ function create_component_lone_return_if_statement(node, render_nodes) {
1148
1011
  consequent: set_loc(
1149
1012
  /** @type {any} */ ({
1150
1013
  type: 'BlockStatement',
1151
- body: [create_component_return_statement(render_nodes, consequent_body[0])],
1014
+ body: [create_component_return_statement(render_nodes, consequent_body[0], false)],
1152
1015
  metadata: { path: [] },
1153
1016
  }),
1154
1017
  node.consequent,
@@ -1160,58 +1023,271 @@ function create_component_lone_return_if_statement(node, render_nodes) {
1160
1023
  );
1161
1024
  }
1162
1025
 
1163
- const TEMPLATE_FRAGMENT_ERROR =
1164
- 'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
1165
-
1166
1026
  /**
1167
1027
  * @param {any} node
1028
+ * @param {any[]} render_nodes
1168
1029
  * @param {TransformContext} transform_context
1169
1030
  * @returns {any}
1170
1031
  */
1171
- function to_jsx_element(node, transform_context) {
1172
- if (node.type === 'JSXElement') return node;
1173
- if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
1174
- throw new Error(
1175
- `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
1176
- );
1177
- }
1178
- if (!node.id) {
1179
- throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
1180
- }
1181
- if (is_dynamic_element_id(node.id)) {
1182
- return dynamic_element_to_jsx_child(node, transform_context);
1183
- }
1032
+ function create_component_returning_if_statement(node, render_nodes, transform_context) {
1033
+ const consequent_body = get_if_consequent_body(node);
1034
+ const branch_statements = build_render_statements(consequent_body, true, transform_context);
1035
+ prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
1184
1036
 
1185
- const name = identifier_to_jsx_name(node.id);
1186
- const attributes = transform_element_attributes_dispatch(
1187
- node.attributes || [],
1188
- transform_context,
1037
+ return set_loc(
1038
+ /** @type {any} */ ({
1039
+ type: 'IfStatement',
1040
+ test: node.test,
1041
+ consequent: set_loc(
1042
+ /** @type {any} */ ({
1043
+ type: 'BlockStatement',
1044
+ body: branch_statements,
1045
+ metadata: { path: [] },
1046
+ }),
1047
+ node.consequent,
1048
+ ),
1049
+ alternate: null,
1050
+ metadata: { path: [] },
1051
+ }),
1189
1052
  node,
1190
1053
  );
1191
- const selfClosing = !!node.selfClosing;
1192
- const children = create_element_children(node.children || [], transform_context);
1193
- const has_unmappable_attribute = attributes.some(
1194
- (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
1195
- );
1054
+ }
1196
1055
 
1197
- /** @type {ESTreeJSX.JSXOpeningElement} */
1198
- const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
1199
- has_unmappable_attribute
1200
- ? {
1201
- type: 'JSXOpeningElement',
1202
- name,
1203
- attributes,
1204
- selfClosing,
1205
- metadata: { path: [] },
1206
- }
1207
- : set_loc(
1208
- /** @type {any} */ ({
1209
- type: 'JSXOpeningElement',
1210
- name,
1211
- attributes,
1212
- selfClosing,
1213
- }),
1214
- node.openingElement || node,
1056
+ /**
1057
+ * @param {any} node
1058
+ * @param {any[]} continuation_body
1059
+ * @param {any[]} render_nodes
1060
+ * @param {TransformContext} transform_context
1061
+ * @returns {any[]}
1062
+ */
1063
+ function create_component_helper_split_returning_if_statements(
1064
+ node,
1065
+ continuation_body,
1066
+ render_nodes,
1067
+ transform_context,
1068
+ ) {
1069
+ const consequent_body = get_if_consequent_body(node);
1070
+ const return_index = consequent_body.findIndex(is_bare_return_statement);
1071
+ const branch_body =
1072
+ return_index === -1 ? consequent_body : consequent_body.slice(0, return_index);
1073
+ const branch_helper = create_hook_safe_helper(
1074
+ branch_body,
1075
+ undefined,
1076
+ node.consequent,
1077
+ transform_context,
1078
+ );
1079
+ const continuation_helper = create_hook_safe_helper(
1080
+ continuation_body,
1081
+ undefined,
1082
+ node,
1083
+ transform_context,
1084
+ );
1085
+ return [
1086
+ set_loc(
1087
+ /** @type {any} */ ({
1088
+ type: 'IfStatement',
1089
+ test: node.test,
1090
+ consequent: set_loc(
1091
+ /** @type {any} */ ({
1092
+ type: 'BlockStatement',
1093
+ body: [
1094
+ ...branch_helper.setup_statements,
1095
+ {
1096
+ type: 'ReturnStatement',
1097
+ argument: combine_render_return_argument(
1098
+ render_nodes,
1099
+ branch_helper.component_element,
1100
+ ),
1101
+ metadata: { path: [] },
1102
+ },
1103
+ ],
1104
+ metadata: { path: [] },
1105
+ }),
1106
+ node.consequent,
1107
+ ),
1108
+ alternate: null,
1109
+ metadata: { path: [] },
1110
+ }),
1111
+ node,
1112
+ ),
1113
+ ...continuation_helper.setup_statements,
1114
+ {
1115
+ type: 'ReturnStatement',
1116
+ argument: combine_render_return_argument(render_nodes, continuation_helper.component_element),
1117
+ metadata: { path: [] },
1118
+ },
1119
+ ];
1120
+ }
1121
+
1122
+ /**
1123
+ * @param {any[]} statements
1124
+ * @param {any[]} render_nodes
1125
+ * @returns {void}
1126
+ */
1127
+ function prepend_render_nodes_to_return_statements(statements, render_nodes) {
1128
+ if (render_nodes.length === 0) {
1129
+ return;
1130
+ }
1131
+
1132
+ for (const statement of statements) {
1133
+ prepend_render_nodes_to_return_statement(statement, render_nodes, false);
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * @param {any} node
1139
+ * @param {any[]} render_nodes
1140
+ * @param {boolean} inside_nested_function
1141
+ * @returns {void}
1142
+ */
1143
+ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nested_function) {
1144
+ if (!node || typeof node !== 'object') {
1145
+ return;
1146
+ }
1147
+
1148
+ if (
1149
+ node.type === 'FunctionDeclaration' ||
1150
+ node.type === 'FunctionExpression' ||
1151
+ node.type === 'ArrowFunctionExpression'
1152
+ ) {
1153
+ inside_nested_function = true;
1154
+ }
1155
+
1156
+ if (!inside_nested_function && node.type === 'ReturnStatement') {
1157
+ node.argument = combine_render_return_argument(render_nodes, node.argument);
1158
+ return;
1159
+ }
1160
+
1161
+ if (Array.isArray(node)) {
1162
+ for (const child of node) {
1163
+ prepend_render_nodes_to_return_statement(child, render_nodes, inside_nested_function);
1164
+ }
1165
+ return;
1166
+ }
1167
+
1168
+ for (const key of Object.keys(node)) {
1169
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1170
+ continue;
1171
+ }
1172
+ prepend_render_nodes_to_return_statement(node[key], render_nodes, inside_nested_function);
1173
+ }
1174
+ }
1175
+
1176
+ /**
1177
+ * @param {any[]} render_nodes
1178
+ * @param {any} return_argument
1179
+ * @returns {any}
1180
+ */
1181
+ function combine_render_return_argument(render_nodes, return_argument) {
1182
+ const combined = render_nodes.map((node) => clone_expression_node_without_locations(node));
1183
+
1184
+ if (!is_null_literal(return_argument)) {
1185
+ combined.push(return_argument_to_render_node(return_argument));
1186
+ }
1187
+
1188
+ return build_return_expression(combined) || create_null_literal();
1189
+ }
1190
+
1191
+ /**
1192
+ * @param {any} argument
1193
+ * @returns {any}
1194
+ */
1195
+ function return_argument_to_render_node(argument) {
1196
+ if (
1197
+ argument?.type === 'JSXElement' ||
1198
+ argument?.type === 'JSXFragment' ||
1199
+ argument?.type === 'JSXExpressionContainer'
1200
+ ) {
1201
+ return argument;
1202
+ }
1203
+
1204
+ return to_jsx_expression_container(argument);
1205
+ }
1206
+
1207
+ /**
1208
+ * @param {any} node
1209
+ * @returns {boolean}
1210
+ */
1211
+ function is_null_literal(node) {
1212
+ return node?.type === 'Literal' && node.value == null;
1213
+ }
1214
+
1215
+ /**
1216
+ * @param {any} node
1217
+ * @returns {any}
1218
+ */
1219
+ function clone_expression_node_without_locations(node) {
1220
+ if (!node || typeof node !== 'object') return node;
1221
+ if (Array.isArray(node)) return node.map(clone_expression_node_without_locations);
1222
+
1223
+ const clone = { ...node };
1224
+ delete clone.loc;
1225
+ delete clone.start;
1226
+ delete clone.end;
1227
+
1228
+ for (const key of Object.keys(clone)) {
1229
+ if (key === 'metadata') {
1230
+ clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
1231
+ continue;
1232
+ }
1233
+ clone[key] = clone_expression_node_without_locations(clone[key]);
1234
+ }
1235
+
1236
+ return clone;
1237
+ }
1238
+
1239
+ const TEMPLATE_FRAGMENT_ERROR =
1240
+ 'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
1241
+
1242
+ /**
1243
+ * @param {any} node
1244
+ * @param {TransformContext} transform_context
1245
+ * @returns {any}
1246
+ */
1247
+ function to_jsx_element(node, transform_context) {
1248
+ if (node.type === 'JSXElement') return node;
1249
+ if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
1250
+ throw new Error(
1251
+ `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
1252
+ );
1253
+ }
1254
+ if (!node.id) {
1255
+ throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
1256
+ }
1257
+ if (is_dynamic_element_id(node.id)) {
1258
+ return dynamic_element_to_jsx_child(node, transform_context);
1259
+ }
1260
+
1261
+ const name = identifier_to_jsx_name(node.id);
1262
+ const attributes = transform_element_attributes_dispatch(
1263
+ node.attributes || [],
1264
+ transform_context,
1265
+ node,
1266
+ );
1267
+ const selfClosing = !!node.selfClosing;
1268
+ const children = create_element_children(node.children || [], transform_context);
1269
+ const has_unmappable_attribute = attributes.some(
1270
+ (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
1271
+ );
1272
+
1273
+ /** @type {ESTreeJSX.JSXOpeningElement} */
1274
+ const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
1275
+ has_unmappable_attribute
1276
+ ? {
1277
+ type: 'JSXOpeningElement',
1278
+ name,
1279
+ attributes,
1280
+ selfClosing,
1281
+ metadata: { path: [] },
1282
+ }
1283
+ : set_loc(
1284
+ /** @type {any} */ ({
1285
+ type: 'JSXOpeningElement',
1286
+ name,
1287
+ attributes,
1288
+ selfClosing,
1289
+ }),
1290
+ node.openingElement || node,
1215
1291
  )
1216
1292
  );
1217
1293
 
@@ -1349,77 +1425,10 @@ function statement_body_to_jsx_child(body_nodes, transform_context) {
1349
1425
  */
1350
1426
  function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1351
1427
  const source_node = get_body_source_node(body_nodes);
1352
- const helper_id = set_loc(
1353
- create_generated_identifier(create_local_statement_component_name(transform_context)),
1354
- source_node,
1355
- );
1356
- const helper_bindings = Array.from(transform_context.available_bindings.values());
1357
-
1358
- // Save and isolate bindings for the helper body
1359
- const saved_bindings = transform_context.available_bindings;
1360
- transform_context.available_bindings = new Map(saved_bindings);
1361
-
1362
- const helper_fn = set_loc(
1363
- /** @type {any} */ ({
1364
- type: 'FunctionDeclaration',
1365
- id: helper_id,
1366
- params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1367
- body: {
1368
- type: 'BlockStatement',
1369
- body: build_render_statements(body_nodes, true, transform_context),
1370
- metadata: { path: [] },
1371
- },
1372
- async: false,
1373
- generator: false,
1374
- metadata: {
1375
- path: [],
1376
- is_component: true,
1377
- is_method: false,
1378
- },
1379
- }),
1380
- source_node,
1381
- );
1382
-
1383
- // Restore bindings
1384
- transform_context.available_bindings = saved_bindings;
1385
-
1386
- // Register helper for hoisting to module level
1387
- if (transform_context.helper_state) {
1388
- transform_context.helper_state.helpers.push(helper_fn);
1389
-
1390
- return to_jsx_expression_container(
1391
- /** @type {any} */ (create_helper_component_element(helper_id, helper_bindings, source_node)),
1392
- source_node,
1393
- );
1394
- }
1428
+ const helper = create_hook_safe_helper(body_nodes, undefined, source_node, transform_context);
1395
1429
 
1396
1430
  return to_jsx_expression_container(
1397
- /** @type {any} */ ({
1398
- type: 'CallExpression',
1399
- callee: {
1400
- type: 'ArrowFunctionExpression',
1401
- params: [],
1402
- body: /** @type {any} */ ({
1403
- type: 'BlockStatement',
1404
- body: [
1405
- helper_fn,
1406
- {
1407
- type: 'ReturnStatement',
1408
- argument: create_helper_component_element(helper_id, helper_bindings, source_node),
1409
- metadata: { path: [] },
1410
- },
1411
- ],
1412
- metadata: { path: [] },
1413
- }),
1414
- async: false,
1415
- generator: false,
1416
- expression: false,
1417
- metadata: { path: [] },
1418
- },
1419
- arguments: [],
1420
- optional: false,
1421
- metadata: { path: [] },
1422
- }),
1431
+ create_hook_safe_helper_iife(helper.setup_statements, helper.component_element),
1423
1432
  source_node,
1424
1433
  );
1425
1434
  }
@@ -1448,49 +1457,102 @@ function create_local_statement_component_name(transform_context) {
1448
1457
  */
1449
1458
  function hook_safe_render_statements(body_nodes, key_expression, transform_context) {
1450
1459
  const source_node = get_body_source_node(body_nodes);
1451
- const helper_id = set_loc(
1452
- create_generated_identifier(create_local_statement_component_name(transform_context)),
1460
+ const helper = create_hook_safe_helper(
1461
+ body_nodes,
1462
+ key_expression,
1453
1463
  source_node,
1464
+ transform_context,
1465
+ );
1466
+ const statements = [...helper.setup_statements];
1467
+
1468
+ statements.push({
1469
+ type: 'ReturnStatement',
1470
+ argument: helper.component_element,
1471
+ metadata: { path: [] },
1472
+ });
1473
+
1474
+ return statements;
1475
+ }
1476
+
1477
+ /**
1478
+ * @param {any[]} body_nodes
1479
+ * @param {Map<string, AST.Identifier>} available_bindings
1480
+ * @returns {AST.Identifier[]}
1481
+ */
1482
+ function get_referenced_helper_bindings(body_nodes, available_bindings) {
1483
+ const helper_bindings = [];
1484
+ const local_bindings = new Map();
1485
+
1486
+ for (const node of body_nodes) {
1487
+ collect_statement_bindings(node, local_bindings);
1488
+ }
1489
+
1490
+ for (const [name, binding] of available_bindings) {
1491
+ if (local_bindings.has(name)) continue;
1492
+
1493
+ if (references_scope_bindings(body_nodes, new Map([[name, binding]]))) {
1494
+ helper_bindings.push(binding);
1495
+ }
1496
+ }
1497
+
1498
+ return helper_bindings;
1499
+ }
1500
+
1501
+ /**
1502
+ * @param {any[]} body_nodes
1503
+ * @param {any} key_expression
1504
+ * @param {any} source_node
1505
+ * @param {TransformContext} transform_context
1506
+ * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
1507
+ */
1508
+ function create_hook_safe_helper(body_nodes, key_expression, source_node, transform_context) {
1509
+ const helper_id = create_generated_identifier(
1510
+ create_local_statement_component_name(transform_context),
1454
1511
  );
1455
- const helper_bindings = Array.from(transform_context.available_bindings.values());
1512
+ const helper_bindings = get_referenced_helper_bindings(
1513
+ body_nodes,
1514
+ transform_context.available_bindings,
1515
+ );
1516
+ const aliases = helper_bindings.map((binding) =>
1517
+ create_helper_type_alias_declaration(helper_id, binding),
1518
+ );
1519
+ const props_type =
1520
+ helper_bindings.length > 0 ? create_helper_props_type_literal(helper_bindings, aliases) : null;
1521
+ const params =
1522
+ props_type !== null ? [create_typed_helper_props_pattern(helper_bindings, props_type)] : [];
1456
1523
 
1457
- // Save and isolate bindings for the helper body
1458
1524
  const saved_bindings = transform_context.available_bindings;
1459
1525
  transform_context.available_bindings = new Map(saved_bindings);
1460
1526
 
1461
- const helper_fn = set_loc(
1462
- /** @type {any} */ ({
1463
- type: 'FunctionDeclaration',
1464
- id: helper_id,
1465
- params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1466
- body: {
1467
- type: 'BlockStatement',
1468
- body: build_render_statements(body_nodes, true, transform_context),
1469
- metadata: { path: [] },
1470
- },
1471
- async: false,
1472
- generator: false,
1473
- metadata: {
1474
- path: [],
1475
- is_component: true,
1476
- is_method: false,
1477
- },
1478
- }),
1479
- source_node,
1480
- );
1527
+ const helper_fn = /** @type {any} */ ({
1528
+ type: 'FunctionExpression',
1529
+ id: clone_identifier(helper_id),
1530
+ params,
1531
+ body: {
1532
+ type: 'BlockStatement',
1533
+ body: build_render_statements(body_nodes, true, transform_context),
1534
+ metadata: { path: [] },
1535
+ },
1536
+ async: false,
1537
+ generator: false,
1538
+ metadata: {
1539
+ path: [],
1540
+ is_component: true,
1541
+ is_method: false,
1542
+ },
1543
+ });
1481
1544
 
1482
- // Restore bindings
1483
1545
  transform_context.available_bindings = saved_bindings;
1484
1546
 
1485
- // Register helper for hoisting to module level
1486
- if (transform_context.helper_state) {
1487
- transform_context.helper_state.helpers.push(helper_fn);
1488
- }
1489
-
1490
1547
  const component_element = create_helper_component_element(
1491
1548
  helper_id,
1492
1549
  helper_bindings,
1493
1550
  source_node,
1551
+ {
1552
+ mapWrapper: false,
1553
+ mapBindingNames: false,
1554
+ mapBindingValues: false,
1555
+ },
1494
1556
  );
1495
1557
 
1496
1558
  if (key_expression) {
@@ -1504,26 +1566,205 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1504
1566
  );
1505
1567
  }
1506
1568
 
1507
- // When helper_state is null (no enclosing component context), inline the
1508
- // helper via an IIFE so the function declaration isn't silently dropped.
1509
1569
  if (!transform_context.helper_state) {
1510
- return [
1511
- helper_fn,
1512
- {
1513
- type: 'ReturnStatement',
1514
- argument: component_element,
1515
- metadata: { path: [] },
1516
- },
1517
- ];
1570
+ return {
1571
+ setup_statements: [
1572
+ ...aliases.map((alias) => alias.declaration),
1573
+ create_helper_function_declaration_from_expression(helper_id, helper_fn),
1574
+ ],
1575
+ component_element,
1576
+ };
1518
1577
  }
1519
1578
 
1520
- return [
1521
- {
1522
- type: 'ReturnStatement',
1523
- argument: component_element,
1579
+ const cache_id = create_generated_identifier(
1580
+ `${transform_context.helper_state.base_name}__${helper_id.name}`,
1581
+ );
1582
+ transform_context.helper_state.helpers.push(create_helper_cache_declaration(cache_id));
1583
+
1584
+ return {
1585
+ setup_statements: [
1586
+ ...aliases.map((alias) => alias.declaration),
1587
+ create_cached_helper_declaration(helper_id, cache_id, helper_fn),
1588
+ ],
1589
+ component_element,
1590
+ };
1591
+ }
1592
+
1593
+ /**
1594
+ * @param {any[]} setup_statements
1595
+ * @param {ESTreeJSX.JSXElement} component_element
1596
+ * @returns {any}
1597
+ */
1598
+ function create_hook_safe_helper_iife(setup_statements, component_element) {
1599
+ return /** @type {any} */ ({
1600
+ type: 'CallExpression',
1601
+ callee: {
1602
+ type: 'ArrowFunctionExpression',
1603
+ params: [],
1604
+ body: /** @type {any} */ ({
1605
+ type: 'BlockStatement',
1606
+ body: [
1607
+ ...setup_statements,
1608
+ {
1609
+ type: 'ReturnStatement',
1610
+ argument: component_element,
1611
+ metadata: { path: [] },
1612
+ },
1613
+ ],
1614
+ metadata: { path: [] },
1615
+ }),
1616
+ async: false,
1617
+ generator: false,
1618
+ expression: false,
1524
1619
  metadata: { path: [] },
1525
1620
  },
1526
- ];
1621
+ arguments: [],
1622
+ optional: false,
1623
+ metadata: { path: [] },
1624
+ });
1625
+ }
1626
+
1627
+ /**
1628
+ * @param {AST.Identifier} helper_id
1629
+ * @param {AST.Identifier} binding
1630
+ * @returns {{ id: AST.Identifier, declaration: any }}
1631
+ */
1632
+ function create_helper_type_alias_declaration(helper_id, binding) {
1633
+ const alias_id = create_generated_identifier(`_tsrx_${helper_id.name}_${binding.name}`);
1634
+
1635
+ return {
1636
+ id: alias_id,
1637
+ declaration: /** @type {any} */ ({
1638
+ type: 'VariableDeclaration',
1639
+ kind: 'const',
1640
+ declarations: [
1641
+ {
1642
+ type: 'VariableDeclarator',
1643
+ id: clone_identifier(alias_id),
1644
+ init: create_generated_identifier(binding.name),
1645
+ metadata: { path: [] },
1646
+ },
1647
+ ],
1648
+ metadata: { path: [] },
1649
+ }),
1650
+ };
1651
+ }
1652
+
1653
+ /**
1654
+ * @param {AST.Identifier[]} bindings
1655
+ * @param {{ id: AST.Identifier }[]} aliases
1656
+ * @returns {any}
1657
+ */
1658
+ function create_helper_props_type_literal(bindings, aliases) {
1659
+ return /** @type {any} */ ({
1660
+ type: 'TSTypeLiteral',
1661
+ members: bindings.map(
1662
+ (binding, i) =>
1663
+ /** @type {any} */ ({
1664
+ type: 'TSPropertySignature',
1665
+ key: create_generated_identifier(binding.name),
1666
+ computed: false,
1667
+ optional: false,
1668
+ readonly: false,
1669
+ static: false,
1670
+ kind: 'init',
1671
+ typeAnnotation: {
1672
+ type: 'TSTypeAnnotation',
1673
+ typeAnnotation: {
1674
+ type: 'TSTypeQuery',
1675
+ exprName: clone_identifier(aliases[i].id),
1676
+ typeArguments: null,
1677
+ metadata: { path: [] },
1678
+ },
1679
+ metadata: { path: [] },
1680
+ },
1681
+ metadata: { path: [] },
1682
+ }),
1683
+ ),
1684
+ metadata: { path: [] },
1685
+ });
1686
+ }
1687
+
1688
+ /**
1689
+ * @param {AST.Identifier[]} bindings
1690
+ * @param {any} props_type
1691
+ * @returns {AST.ObjectPattern}
1692
+ */
1693
+ function create_typed_helper_props_pattern(bindings, props_type) {
1694
+ const pattern = create_helper_props_pattern(bindings);
1695
+ /** @type {any} */ (pattern).typeAnnotation = {
1696
+ type: 'TSTypeAnnotation',
1697
+ typeAnnotation: props_type,
1698
+ metadata: { path: [] },
1699
+ };
1700
+ return pattern;
1701
+ }
1702
+
1703
+ /**
1704
+ * @param {AST.Identifier} cache_id
1705
+ * @returns {any}
1706
+ */
1707
+ function create_helper_cache_declaration(cache_id) {
1708
+ return /** @type {any} */ ({
1709
+ type: 'VariableDeclaration',
1710
+ kind: 'let',
1711
+ declarations: [
1712
+ {
1713
+ type: 'VariableDeclarator',
1714
+ id: clone_identifier(cache_id),
1715
+ init: null,
1716
+ metadata: { path: [] },
1717
+ },
1718
+ ],
1719
+ metadata: { path: [] },
1720
+ });
1721
+ }
1722
+
1723
+ /**
1724
+ * @param {AST.Identifier} helper_id
1725
+ * @param {AST.Identifier} cache_id
1726
+ * @param {any} helper_fn
1727
+ * @returns {any}
1728
+ */
1729
+ function create_cached_helper_declaration(helper_id, cache_id, helper_fn) {
1730
+ return /** @type {any} */ ({
1731
+ type: 'VariableDeclaration',
1732
+ kind: 'const',
1733
+ declarations: [
1734
+ {
1735
+ type: 'VariableDeclarator',
1736
+ id: clone_identifier(helper_id),
1737
+ init: {
1738
+ type: 'LogicalExpression',
1739
+ operator: '??',
1740
+ left: clone_identifier(cache_id),
1741
+ right: {
1742
+ type: 'AssignmentExpression',
1743
+ operator: '=',
1744
+ left: clone_identifier(cache_id),
1745
+ right: helper_fn,
1746
+ metadata: { path: [] },
1747
+ },
1748
+ metadata: { path: [] },
1749
+ },
1750
+ metadata: { path: [] },
1751
+ },
1752
+ ],
1753
+ metadata: { path: [] },
1754
+ });
1755
+ }
1756
+
1757
+ /**
1758
+ * @param {AST.Identifier} helper_id
1759
+ * @param {any} helper_fn
1760
+ * @returns {AST.FunctionDeclaration}
1761
+ */
1762
+ function create_helper_function_declaration_from_expression(helper_id, helper_fn) {
1763
+ return /** @type {any} */ ({
1764
+ ...helper_fn,
1765
+ type: 'FunctionDeclaration',
1766
+ id: clone_identifier(helper_id),
1767
+ });
1527
1768
  }
1528
1769
 
1529
1770
  /**