@tsrx/core 0.1.4 → 0.1.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.
@@ -21,6 +21,7 @@ import {
21
21
  get_for_of_iteration_params,
22
22
  identifier_to_jsx_name,
23
23
  is_bare_render_expression,
24
+ is_component_jsx_name,
24
25
  is_dynamic_element_id,
25
26
  is_jsx_child,
26
27
  set_loc,
@@ -33,12 +34,7 @@ import {
33
34
  jsx_id as build_jsx_id,
34
35
  } from '../../utils/builders.js';
35
36
  import * as b from '../../utils/builders.js';
36
- import {
37
- apply_lazy_transforms,
38
- collect_lazy_bindings_from_component,
39
- preallocate_lazy_ids,
40
- replace_lazy_params,
41
- } from '../lazy.js';
37
+ import { apply_lazy_transforms, preallocate_lazy_ids } from '../lazy.js';
42
38
  import { find_first_top_level_await_in_component_body } from '../await.js';
43
39
  import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
44
40
  import {
@@ -49,7 +45,7 @@ import {
49
45
  validate_component_return_statement,
50
46
  validate_component_unsupported_loop_statement,
51
47
  } from '../../analyze/validation.js';
52
- import { get_component_from_path } from '../../utils/ast.js';
48
+ import { get_component_from_path, is_function_or_component_node } from '../../utils/ast.js';
53
49
  import {
54
50
  is_interleaved_body as is_interleaved_body_core,
55
51
  is_capturable_jsx_child,
@@ -245,6 +241,8 @@ export function createJsxTransform(platform) {
245
241
  needs_ref_prop: false,
246
242
  needs_normalize_spread_props: false,
247
243
  needs_fragment: false,
244
+ needs_for_of_iterable: false,
245
+ needs_iteration_value_type: false,
248
246
  module_scoped_hook_components:
249
247
  options?.moduleScopedHookComponents ?? !!platform.hooks?.moduleScopedHookComponents,
250
248
  helper_state: null,
@@ -515,7 +513,7 @@ export function createJsxTransform(platform) {
515
513
  const value = state.current_css_hash
516
514
  ? `${state.current_css_hash} ${class_name}`
517
515
  : class_name;
518
- return /** @type {any} */ (b.literal(value, node));
516
+ return b.literal(value, undefined, node);
519
517
  },
520
518
 
521
519
  // Default .metadata on every function-like node so downstream consumers
@@ -643,15 +641,6 @@ export function component_to_function_declaration(component, transform_context,
643
641
  // Collect param bindings from original patterns (lazy patterns still intact).
644
642
  const param_bindings = collect_param_bindings(params);
645
643
 
646
- // Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
647
- // for later replacement. Body bindings (count, setCount, etc.) are still in the
648
- // original patterns, so collect_statement_bindings during build will find them.
649
- // In type-only mode the lazy rewrite is skipped entirely so destructuring
650
- // patterns survive into the virtual TSX and TypeScript can flow real types.
651
- const lazy_bindings = transform_context.typeOnly
652
- ? new Map()
653
- : collect_lazy_bindings_from_component(params, body, transform_context);
654
-
655
644
  // Save and set context for this component scope
656
645
  const saved_helper_state = transform_context.helper_state;
657
646
  const saved_bindings = transform_context.available_bindings;
@@ -659,66 +648,44 @@ export function component_to_function_declaration(component, transform_context,
659
648
  transform_context.available_bindings = new Map(param_bindings);
660
649
 
661
650
  const body_statements = build_component_statements(body, transform_context);
662
-
663
- // Replace lazy param patterns with generated identifiers
664
- const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
665
-
666
- // Wrap body_statements in a BlockStatement so that apply_lazy_transforms
667
- // runs collect_block_shadowed_names and detects body-level declarations
668
- // (e.g. `const name = ...`) that shadow lazy binding names.
669
- const body_block = /** @type {any} */ ({
670
- type: 'BlockStatement',
671
- body: body_statements,
672
- metadata: { path: [] },
673
- });
674
- const final_body =
675
- lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
651
+ const body_block = b.block(body_statements);
676
652
 
677
653
  /** @type {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression} */
678
654
  let fn;
679
655
 
680
656
  if (component.id) {
681
- fn = /** @type {any} */ ({
682
- type: 'FunctionDeclaration',
683
- id: component.id,
684
- typeParameters: component.typeParameters,
685
- params: final_params,
686
- body: final_body,
687
- async: is_async_component,
688
- generator: false,
689
- metadata: {
690
- path: [],
691
- is_component: true,
692
- },
693
- });
657
+ fn = b.function_declaration(
658
+ component.id,
659
+ params,
660
+ body_block,
661
+ is_async_component,
662
+ component.typeParameters,
663
+ );
694
664
  } else if (component.metadata?.arrow) {
695
- fn = /** @type {any} */ ({
696
- type: 'ArrowFunctionExpression',
697
- typeParameters: component.typeParameters,
698
- params: final_params,
699
- body: final_body,
700
- async: is_async_component,
701
- generator: false,
702
- expression: false,
703
- metadata: {
704
- path: [],
705
- is_component: true,
706
- },
707
- });
665
+ fn = b.arrow(params, body_block, is_async_component, component.typeParameters);
708
666
  } else {
709
- fn = /** @type {any} */ ({
710
- type: 'FunctionExpression',
711
- id: null,
712
- typeParameters: component.typeParameters,
713
- params: final_params,
714
- body: final_body,
715
- async: is_async_component,
716
- generator: false,
717
- metadata: {
718
- path: [],
719
- is_component: true,
720
- },
721
- });
667
+ fn = b.function(null, params, body_block, is_async_component, component.typeParameters);
668
+ }
669
+ /** @type {any} */ (fn.metadata).is_component = true;
670
+
671
+ // `preallocate_lazy_ids` stamped `has_lazy_descendants` on the source
672
+ // `Component` node; the freshly-built `fn` shares the same params/body
673
+ // subtree, so the flag is equally applicable. Propagating it lets
674
+ // `apply_lazy_transforms` honor its constant-time early-return path.
675
+ if (/** @type {any} */ (component).metadata?.has_lazy_descendants) {
676
+ /** @type {any} */ (fn.metadata).has_lazy_descendants = true;
677
+ }
678
+
679
+ // Apply lazy `&{}` / `&[]` rewrites end-to-end: the function-handler in
680
+ // `apply_lazy_transforms` collects param bindings, merges with body bindings
681
+ // discovered by the BlockStatement handler, replaces lazy params with their
682
+ // `__lazyN` ids, and rewrites every reference. Constant-time fast-path for
683
+ // functions whose subtrees contain no lazy patterns (flagged ahead of time
684
+ // by `preallocate_lazy_ids`). In type-only mode the rewrite is skipped so
685
+ // destructuring patterns survive into the virtual TSX and TypeScript can
686
+ // flow real types.
687
+ if (!transform_context.typeOnly) {
688
+ fn = /** @type {typeof fn} */ (apply_lazy_transforms(fn, new Map()));
722
689
  }
723
690
 
724
691
  // Restore context
@@ -889,25 +856,16 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
889
856
  if (stmt.type === 'ReturnStatement') {
890
857
  if (stmt.argument) {
891
858
  render_nodes.push(
892
- /** @type {any} */ ({
893
- type: 'JSXExpressionContainer',
894
- expression: set_loc(
895
- /** @type {any} */ ({
896
- type: 'ConditionalExpression',
897
- test: clone_expression_node(child.test),
898
- consequent: {
899
- type: 'Literal',
900
- value: null,
901
- raw: 'null',
902
- metadata: { path: [] },
903
- },
904
- alternate: stmt.argument,
905
- metadata: { path: [] },
906
- }),
859
+ b.jsx_expression_container(
860
+ set_loc(
861
+ b.conditional(
862
+ clone_expression_node(child.test),
863
+ b.literal(null),
864
+ stmt.argument,
865
+ ),
907
866
  child,
908
867
  ),
909
- metadata: { path: [] },
910
- }),
868
+ ),
911
869
  );
912
870
  }
913
871
  } else {
@@ -928,26 +886,6 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
928
886
  continue;
929
887
  }
930
888
 
931
- if (
932
- child.type === 'IfStatement' &&
933
- !child.alternate &&
934
- !is_returning_if_statement(child) &&
935
- !transform_context.platform.hooks?.isTopLevelSetupCall &&
936
- body_contains_top_level_hook_call([child], transform_context, true) &&
937
- i + 1 < body_nodes.length
938
- ) {
939
- statements.push(
940
- ...create_continuation_lift_if_statement(
941
- child,
942
- body_nodes.slice(i + 1),
943
- render_nodes,
944
- transform_context,
945
- ),
946
- );
947
- transform_context.available_bindings = saved_bindings;
948
- return statements;
949
- }
950
-
951
889
  if (
952
890
  child.type === 'ForOfStatement' &&
953
891
  !child.await &&
@@ -959,26 +897,9 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
959
897
  true,
960
898
  )
961
899
  ) {
962
- const for_of_continuation = body_nodes.slice(i + 1);
963
- const hoisted = build_hoisted_for_of_with_hooks(
964
- child,
965
- for_of_continuation,
966
- transform_context,
967
- );
900
+ const hoisted = build_hoisted_for_of_with_hooks(child, transform_context);
968
901
  if (hoisted) {
969
902
  statements.push(...hoisted.hoist_statements);
970
- if (for_of_continuation.length > 0) {
971
- // Tail was lifted into the helper; everything after the for-of
972
- // now lives there. Combine prior render_nodes with the iteration
973
- // JSX and return.
974
- statements.push({
975
- type: 'ReturnStatement',
976
- argument: combine_render_return_argument(render_nodes, hoisted.jsx_child),
977
- metadata: { path: [] },
978
- });
979
- transform_context.available_bindings = saved_bindings;
980
- return statements;
981
- }
982
903
  if (interleaved && is_capturable_jsx_child(hoisted.jsx_child)) {
983
904
  const { declaration, reference } = captureJsxChild(hoisted.jsx_child, capture_index++);
984
905
  statements.push(declaration);
@@ -990,43 +911,6 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
990
911
  }
991
912
  }
992
913
 
993
- if (
994
- child.type === 'TryStatement' &&
995
- !child.finalizer &&
996
- !transform_context.platform.hooks?.isTopLevelSetupCall &&
997
- try_statement_contains_hooks(child, transform_context) &&
998
- i + 1 < body_nodes.length
999
- ) {
1000
- statements.push(
1001
- ...create_continuation_lift_try_statement(
1002
- child,
1003
- body_nodes.slice(i + 1),
1004
- render_nodes,
1005
- transform_context,
1006
- ),
1007
- );
1008
- transform_context.available_bindings = saved_bindings;
1009
- return statements;
1010
- }
1011
-
1012
- if (
1013
- child.type === 'SwitchStatement' &&
1014
- !transform_context.platform.hooks?.isTopLevelSetupCall &&
1015
- body_contains_top_level_hook_call([child], transform_context, true) &&
1016
- i + 1 < body_nodes.length
1017
- ) {
1018
- statements.push(
1019
- ...create_continuation_lift_switch_statement(
1020
- child,
1021
- body_nodes.slice(i + 1),
1022
- render_nodes,
1023
- transform_context,
1024
- ),
1025
- );
1026
- transform_context.available_bindings = saved_bindings;
1027
- return statements;
1028
- }
1029
-
1030
914
  if (is_jsx_child(child)) {
1031
915
  const jsx = to_jsx_child(child, transform_context);
1032
916
  statements.push(...extract_jsx_setup_declarations(jsx));
@@ -1051,10 +935,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
1051
935
 
1052
936
  const return_arg = build_return_expression(render_nodes);
1053
937
  if (return_arg || (return_null_when_empty && !has_terminal_return)) {
1054
- statements.push({
1055
- type: 'ReturnStatement',
1056
- argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
1057
- });
938
+ statements.push(b.return(return_arg || b.literal(null)));
1058
939
  }
1059
940
 
1060
941
  transform_context.available_bindings = saved_bindings;
@@ -1254,16 +1135,7 @@ function create_helper_props_property(binding) {
1254
1135
  const key = create_generated_identifier(binding.name);
1255
1136
  const value = create_generated_identifier(binding.name);
1256
1137
 
1257
- return /** @type {any} */ ({
1258
- type: 'Property',
1259
- key,
1260
- value,
1261
- kind: 'init',
1262
- method: false,
1263
- shorthand: true,
1264
- computed: false,
1265
- metadata: { path: [] },
1266
- });
1138
+ return b.prop('init', key, value, false, true);
1267
1139
  }
1268
1140
 
1269
1141
  /**
@@ -1499,7 +1371,7 @@ function create_module_scoped_hook_component_id(helper_id, transform_context) {
1499
1371
  * @param {any[]} params
1500
1372
  * @returns {Map<string, AST.Identifier>}
1501
1373
  */
1502
- function collect_param_bindings(params) {
1374
+ export function collect_param_bindings(params) {
1503
1375
  const bindings = new Map();
1504
1376
  for (const param of params) {
1505
1377
  collect_pattern_bindings(param, bindings);
@@ -1512,7 +1384,7 @@ function collect_param_bindings(params) {
1512
1384
  * @param {Map<string, AST.Identifier>} bindings
1513
1385
  * @returns {void}
1514
1386
  */
1515
- function collect_statement_bindings(statement, bindings) {
1387
+ export function collect_statement_bindings(statement, bindings) {
1516
1388
  if (!statement) return;
1517
1389
 
1518
1390
  if (statement.type === 'VariableDeclaration') {
@@ -1646,6 +1518,16 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1646
1518
  const node = render_nodes[i];
1647
1519
  if (node.type !== 'JSXElement') continue;
1648
1520
  if (!is_hoist_safe_jsx_node(node)) continue;
1521
+ if (is_bare_component_invocation(node)) {
1522
+ // `<Helper />` with no attributes and no children is just an
1523
+ // invocation reference — most often a generated `StatementBodyHook`
1524
+ // chain element we emitted ourselves. Hoisting it would produce
1525
+ // `const App__staticN = <Helper />` aliases that bloat the output
1526
+ // without enabling React's element-identity fast path (the helper
1527
+ // isn't memoized, so the parent re-invokes it every render either
1528
+ // way). Inline the reference at the call site instead.
1529
+ continue;
1530
+ }
1649
1531
  if (
1650
1532
  transform_context.platform.hooks?.canHoistStaticNode &&
1651
1533
  !transform_context.platform.hooks.canHoistStaticNode(node, transform_context)
@@ -1663,6 +1545,23 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1663
1545
  }
1664
1546
  }
1665
1547
 
1548
+ /**
1549
+ * `<Helper />` shape with no attributes and no children. The opening element
1550
+ * name must be component-shaped (see `is_component_jsx_name`) — lowercase
1551
+ * identifiers are host DOM tags, which *do* benefit from hoisting because
1552
+ * React diffs them against the previous render.
1553
+ *
1554
+ * @param {any} node
1555
+ * @returns {boolean}
1556
+ */
1557
+ function is_bare_component_invocation(node) {
1558
+ if (!node || node.type !== 'JSXElement') return false;
1559
+ const opening = node.openingElement;
1560
+ if (!opening || opening.attributes.length > 0) return false;
1561
+ if (node.children.length > 0) return false;
1562
+ return is_component_jsx_name(opening.name);
1563
+ }
1564
+
1666
1565
  /**
1667
1566
  * Static JSX that appears before multiple early-return guards is otherwise
1668
1567
  * cloned into every generated return. Capture it once at its source position
@@ -1891,169 +1790,6 @@ function create_component_returning_if_statement(node, render_nodes, transform_c
1891
1790
  return set_loc(b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null), node);
1892
1791
  }
1893
1792
 
1894
- /* ---------------------------------------------------------------------- *
1895
- * Continuation-lift primitives shared across if / switch / try / for-of *
1896
- * ---------------------------------------------------------------------- */
1897
-
1898
- /**
1899
- * Build the helper component that owns the post-control-flow continuation.
1900
- * Same shape as `create_hook_safe_helper`; named for intent at lift call sites.
1901
- *
1902
- * @param {any[]} continuation_body
1903
- * @param {any} source_node
1904
- * @param {TransformContext} transform_context
1905
- * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
1906
- */
1907
- function build_tail_helper(continuation_body, source_node, transform_context) {
1908
- return create_hook_safe_helper(continuation_body, undefined, source_node, transform_context);
1909
- }
1910
-
1911
- /**
1912
- * Clone the tail helper's component element for embedding inside another
1913
- * branch's body. Loses location info because the same element appears in
1914
- * multiple positions and downstream tooling treats AST nodes as identity-keyed.
1915
- *
1916
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1917
- * @returns {any}
1918
- */
1919
- function clone_tail_invocation(tail_helper) {
1920
- return clone_expression_node(tail_helper.component_element, false);
1921
- }
1922
-
1923
- /**
1924
- * Return `[...body, <TailHelper x={x} />]` so the branch's render output
1925
- * includes the tail invocation and the post-hook locals flow forward.
1926
- * Used by if / switch / try (unconditional append). For-of uses a different
1927
- * shape — gating on `_tsrx_isLast_<n>` — so it constructs its own.
1928
- *
1929
- * @param {any[]} body
1930
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1931
- * @returns {any[]}
1932
- */
1933
- function append_tail_invocation(body, tail_helper) {
1934
- return [...body, clone_tail_invocation(tail_helper)];
1935
- }
1936
-
1937
- /**
1938
- * @param {AST.Identifier} tail_synthetic_id
1939
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1940
- * @returns {any}
1941
- */
1942
- function create_loop_tail_expression(tail_synthetic_id, tail_helper) {
1943
- return b.logical('&&', clone_identifier(tail_synthetic_id), clone_tail_invocation(tail_helper));
1944
- }
1945
-
1946
- /**
1947
- * @param {AST.Identifier} tail_synthetic_id
1948
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1949
- * @returns {any}
1950
- */
1951
- function create_loop_tail_conditional(tail_synthetic_id, tail_helper) {
1952
- return b.conditional(
1953
- clone_identifier(tail_synthetic_id),
1954
- clone_tail_invocation(tail_helper),
1955
- create_null_literal(),
1956
- );
1957
- }
1958
-
1959
- /**
1960
- * @param {any[]} statements
1961
- * @param {AST.Identifier} tail_synthetic_id
1962
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1963
- * @returns {void}
1964
- */
1965
- function append_loop_tail_to_return_statements(statements, tail_synthetic_id, tail_helper) {
1966
- for (const statement of statements) {
1967
- append_loop_tail_to_return_statement(statement, tail_synthetic_id, tail_helper, false);
1968
- }
1969
- }
1970
-
1971
- /**
1972
- * @param {any} node
1973
- * @param {AST.Identifier} tail_synthetic_id
1974
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1975
- * @param {boolean} inside_nested_function
1976
- * @returns {void}
1977
- */
1978
- function append_loop_tail_to_return_statement(
1979
- node,
1980
- tail_synthetic_id,
1981
- tail_helper,
1982
- inside_nested_function,
1983
- ) {
1984
- if (!node || typeof node !== 'object') {
1985
- return;
1986
- }
1987
-
1988
- if (
1989
- node.type === 'FunctionDeclaration' ||
1990
- node.type === 'FunctionExpression' ||
1991
- node.type === 'ArrowFunctionExpression'
1992
- ) {
1993
- inside_nested_function = true;
1994
- }
1995
-
1996
- if (!inside_nested_function && node.type === 'ReturnStatement') {
1997
- if (
1998
- references_scope_bindings(
1999
- node.argument,
2000
- new Map([[tail_synthetic_id.name, tail_synthetic_id]]),
2001
- )
2002
- ) {
2003
- return;
2004
- }
2005
- node.argument = append_loop_tail_to_return_argument(
2006
- node.argument,
2007
- tail_synthetic_id,
2008
- tail_helper,
2009
- );
2010
- return;
2011
- }
2012
-
2013
- if (Array.isArray(node)) {
2014
- for (const child of node) {
2015
- append_loop_tail_to_return_statement(
2016
- child,
2017
- tail_synthetic_id,
2018
- tail_helper,
2019
- inside_nested_function,
2020
- );
2021
- }
2022
- return;
2023
- }
2024
-
2025
- for (const key of Object.keys(node)) {
2026
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
2027
- continue;
2028
- }
2029
- append_loop_tail_to_return_statement(
2030
- node[key],
2031
- tail_synthetic_id,
2032
- tail_helper,
2033
- inside_nested_function,
2034
- );
2035
- }
2036
- }
2037
-
2038
- /**
2039
- * @param {any} return_argument
2040
- * @param {AST.Identifier} tail_synthetic_id
2041
- * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
2042
- * @returns {any}
2043
- */
2044
- function append_loop_tail_to_return_argument(return_argument, tail_synthetic_id, tail_helper) {
2045
- if (return_argument == null || is_null_literal(return_argument)) {
2046
- return create_loop_tail_conditional(tail_synthetic_id, tail_helper);
2047
- }
2048
-
2049
- return (
2050
- build_return_expression([
2051
- return_argument_to_render_node(return_argument),
2052
- to_jsx_expression_container(create_loop_tail_expression(tail_synthetic_id, tail_helper)),
2053
- ]) || create_null_literal()
2054
- );
2055
- }
2056
-
2057
1793
  /**
2058
1794
  * Build a `return <combined-render-fragment>;` statement, prepending any
2059
1795
  * `render_nodes` collected before the control-flow construct so they don't
@@ -2134,353 +1870,73 @@ function create_component_helper_split_returning_if_statements(
2134
1870
  }
2135
1871
 
2136
1872
  /**
2137
- * Lift a non-returning `if` whose consequent contains hook calls plus the
2138
- * statements that follow it into helper components.
1873
+ * Hoist the helper for a hook-bearing for-of body out of the iteration
1874
+ * callback so the helper is declared once per render rather than re-bound on
1875
+ * every iteration. Loop-scoped param types are derived from the iteration
1876
+ * source via a TS `type` alias (rather than the const+typeof pattern used
1877
+ * for outer bindings, which would require the loop var to be in scope).
1878
+ *
1879
+ * The iteration source is hoisted into a generated `let` and normalized via
1880
+ * `Array.isArray(src) ? src : Array.from(src)` so any Iterable / ArrayLike
1881
+ * works while skipping the copy when the source is already an array. The
1882
+ * iteration itself is emitted as `source.map((item, i) => ...)`.
2139
1883
  *
2140
- * Without this, the consequent's hook would be wrapped into a child component
2141
- * (StatementBodyHook) but any code after the `if` that reads bindings the hook
2142
- * mutates would observe the pre-hook value, because React commits children
2143
- * after their parent has finished rendering. The fix mirrors the early-return
2144
- * splitter: emit a tail helper that owns the post-`if` statements, append a
2145
- * call to it inside the branch helper so the post-hook bindings flow forward,
2146
- * and render the tail helper directly when the `if` is false.
1884
+ * Bails out (returns null) when the loop pattern is destructured deriving
1885
+ * element types from a tuple/object pattern is more involved and deferred.
2147
1886
  *
2148
- * @param {any} if_node
2149
- * @param {any[]} continuation_body
2150
- * @param {any[]} render_nodes
1887
+ * @param {any} node - ForOfStatement
2151
1888
  * @param {TransformContext} transform_context
2152
- * @returns {any[]}
1889
+ * @returns {{ hoist_statements: any[], jsx_child: any } | null}
2153
1890
  */
2154
- function create_continuation_lift_if_statement(
2155
- if_node,
2156
- continuation_body,
2157
- render_nodes,
2158
- transform_context,
2159
- ) {
2160
- const consequent_body = get_if_consequent_body(if_node);
2161
- const tail_helper = build_tail_helper(continuation_body, if_node, transform_context);
2162
- const branch_helper = create_hook_safe_helper(
2163
- append_tail_invocation(consequent_body, tail_helper),
2164
- undefined,
2165
- if_node.consequent,
2166
- transform_context,
2167
- );
1891
+ function build_hoisted_for_of_with_hooks(node, transform_context) {
1892
+ const loop_params = get_for_of_iteration_params(node.left, node.index);
1893
+ for (const param of loop_params) {
1894
+ if (param.type !== 'Identifier') return null;
1895
+ }
2168
1896
 
2169
- const branch_block = set_loc(
2170
- b.block([
2171
- ...branch_helper.setup_statements,
2172
- combined_return_statement(render_nodes, branch_helper.component_element),
2173
- ]),
2174
- if_node.consequent,
1897
+ const original_loop_body = /** @type {any[]} */ (
1898
+ rewrite_loop_continues_to_bare_returns(
1899
+ node.body.type === 'BlockStatement' ? node.body.body : [node.body],
1900
+ )
2175
1901
  );
2176
1902
 
2177
- return [
2178
- ...tail_helper.setup_statements,
2179
- set_loc(b.if(if_node.test, branch_block, null), if_node),
2180
- combined_return_statement(render_nodes, tail_helper.component_element),
2181
- ];
2182
- }
2183
-
2184
- /**
2185
- * Continuation lift for `try` / `try / pending / catch` statements. Same
2186
- * shape as if/switch: build a tail helper from the post-`try` statements, and
2187
- * append a clone of its invocation to the try body and the catch body so the
2188
- * post-hook locals inside each branch flow forward into the tail. The pending
2189
- * body is left untouched — when Suspense renders the pending fallback the
2190
- * parent's render is unwound, so the tail wouldn't run in source semantics
2191
- * either. Once augmented, the existing try transform builds the
2192
- * Suspense / TsrxErrorBoundary wrapper as usual.
2193
- *
2194
- * @param {any} node - TryStatement
2195
- * @param {any[]} continuation_body
2196
- * @param {any[]} render_nodes
2197
- * @param {TransformContext} transform_context
2198
- * @returns {any[]}
2199
- */
2200
- function create_continuation_lift_try_statement(
2201
- node,
2202
- continuation_body,
2203
- render_nodes,
2204
- transform_context,
2205
- ) {
2206
- const tail_helper = build_tail_helper(continuation_body, node, transform_context);
2207
-
2208
- const augmented_block = {
2209
- ...node.block,
2210
- body: append_tail_invocation(node.block.body || [], tail_helper),
2211
- };
1903
+ const source_id = create_generated_identifier(
1904
+ `_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
1905
+ );
1906
+ const use_iterable_helper = !!transform_context.platform.imports.forOfIterableHelper;
1907
+ const { source_decl, source_normalize_decl } = use_iterable_helper
1908
+ ? {
1909
+ source_decl: b.let(clone_identifier(source_id), clone_expression_node(node.right)),
1910
+ source_normalize_decl: null,
1911
+ }
1912
+ : build_array_normalization_decls(source_id, node.right);
2212
1913
 
2213
- let augmented_handler = node.handler;
2214
- if (node.handler) {
2215
- augmented_handler = {
2216
- ...node.handler,
2217
- body: {
2218
- ...node.handler.body,
2219
- body: append_tail_invocation(node.handler.body.body || [], tail_helper),
2220
- },
2221
- };
1914
+ const saved_bindings = transform_context.available_bindings;
1915
+ transform_context.available_bindings = new Map(saved_bindings);
1916
+ const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
1917
+ for (const param of loop_params) {
1918
+ collect_pattern_bindings(param, transform_context.available_bindings);
2222
1919
  }
1920
+ validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
1921
+ original_loop_body,
1922
+ transform_context,
1923
+ loop_scoped_names,
1924
+ );
2223
1925
 
2224
- const augmented_try = {
2225
- ...node,
2226
- block: augmented_block,
2227
- handler: augmented_handler,
2228
- };
2229
-
2230
- const try_jsx_child = (
2231
- transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
2232
- )(augmented_try, transform_context);
1926
+ const all_helper_bindings = get_referenced_helper_bindings(
1927
+ original_loop_body,
1928
+ transform_context.available_bindings,
1929
+ );
1930
+ const outer_bindings = all_helper_bindings.filter((b) => !loop_scoped_names.has(b.name));
1931
+ const loop_bindings = all_helper_bindings.filter((b) => loop_scoped_names.has(b.name));
2233
1932
 
2234
- return [...tail_helper.setup_statements, combined_return_statement(render_nodes, try_jsx_child)];
2235
- }
2236
-
2237
- /**
2238
- * @param {any} node - TryStatement
2239
- * @param {TransformContext} transform_context
2240
- * @returns {boolean}
2241
- */
2242
- function try_statement_contains_hooks(node, transform_context) {
2243
- if (body_contains_top_level_hook_call(node.block?.body || [], transform_context, true)) {
2244
- return true;
2245
- }
2246
- if (
2247
- node.handler &&
2248
- body_contains_top_level_hook_call(node.handler.body?.body || [], transform_context, true)
2249
- ) {
2250
- return true;
2251
- }
2252
- if (
2253
- node.pending &&
2254
- body_contains_top_level_hook_call(node.pending.body || [], transform_context, true)
2255
- ) {
2256
- return true;
2257
- }
2258
- return false;
2259
- }
2260
-
2261
- /**
2262
- * Continuation lift for `switch` statements. Same shape as the if-version:
2263
- * each case body is wrapped in its own helper component that ends with a
2264
- * call to a shared tail helper, so post-hook bindings inside any case flow
2265
- * forward to the statements after the switch. The fall-through return at
2266
- * the end renders the tail helper directly, covering the case where no
2267
- * `case` (and no `default`) matched.
2268
- *
2269
- * Empty fall-through cases (`case 'a':` with no body, falling through to
2270
- * the next case) are preserved as-is — they must not get their own helper
2271
- * because that would convert fall-through into early-return.
2272
- *
2273
- * @param {any} switch_node
2274
- * @param {any[]} continuation_body
2275
- * @param {any[]} render_nodes
2276
- * @param {TransformContext} transform_context
2277
- * @returns {any[]}
2278
- */
2279
- function create_continuation_lift_switch_statement(
2280
- switch_node,
2281
- continuation_body,
2282
- render_nodes,
2283
- transform_context,
2284
- ) {
2285
- const tail_helper = build_tail_helper(continuation_body, switch_node, transform_context);
2286
-
2287
- // Per-case info computed once: own body (statements before any
2288
- // terminator) and whether the case has a `break` / `return`.
2289
- const case_info = switch_node.cases.map((/** @type {any} */ c) => {
2290
- const consequent = flatten_switch_consequent(c.consequent || []);
2291
- const own_body = [];
2292
- let own_has_terminator = false;
2293
- for (const node of consequent) {
2294
- if (node.type === 'BreakStatement' || node.type === 'ReturnStatement') {
2295
- own_has_terminator = true;
2296
- break;
2297
- }
2298
- own_body.push(node);
2299
- }
2300
- return { own_body, own_has_terminator };
2301
- });
2302
-
2303
- // Allocate helper ids in source order (forward pass) so the snapshot's
2304
- // `StatementBodyHook<N>` numbering reads top-to-bottom by case position.
2305
- /** @type {Array<AST.Identifier | null>} */
2306
- const helper_ids = case_info.map(
2307
- (/** @type {{ own_body: any[], own_has_terminator: boolean }} */ info) =>
2308
- info.own_body.length === 0
2309
- ? null
2310
- : create_generated_identifier(create_local_statement_component_name(transform_context)),
2311
- );
2312
-
2313
- // Build helpers in reverse order: each fall-through case's helper body
2314
- // invokes the *next* case's helper, so the chain forwards post-mutation
2315
- // locals through the switch. Reverse iteration ensures the next helper's
2316
- // component_element is already constructed when we need to embed it.
2317
- /** @type {Array<{ setup_statements: any[], component_element: any } | null>} */
2318
- const case_helper_by_index = new Array(switch_node.cases.length).fill(null);
2319
- for (let i = switch_node.cases.length - 1; i >= 0; i--) {
2320
- const { own_body, own_has_terminator } = case_info[i];
2321
- if (own_body.length === 0) continue;
2322
-
2323
- // Determine the downstream helper this case invokes after its own body.
2324
- // - With a terminator: invoke the tail helper directly (case exits switch).
2325
- // - Otherwise (fall-through): invoke the next non-empty case's helper,
2326
- // or the tail if nothing else follows.
2327
- let downstream;
2328
- if (own_has_terminator) {
2329
- downstream = tail_helper;
2330
- } else {
2331
- let next_helper = null;
2332
- for (let j = i + 1; j < switch_node.cases.length; j++) {
2333
- if (case_helper_by_index[j]) {
2334
- next_helper = case_helper_by_index[j];
2335
- break;
2336
- }
2337
- }
2338
- downstream = next_helper ?? tail_helper;
2339
- }
2340
-
2341
- case_helper_by_index[i] = create_hook_safe_helper(
2342
- append_tail_invocation(own_body, downstream),
2343
- undefined,
2344
- switch_node.cases[i],
2345
- transform_context,
2346
- /** @type {any} */ (helper_ids[i]),
2347
- );
2348
- }
2349
-
2350
- const new_cases = switch_node.cases.map(
2351
- (/** @type {any} */ original_case, /** @type {number} */ i) => {
2352
- const helper = case_helper_by_index[i];
2353
- if (helper) {
2354
- return b.switch_case(original_case.test, [
2355
- combined_return_statement(render_nodes, helper.component_element),
2356
- ]);
2357
- }
2358
-
2359
- const { own_body, own_has_terminator } = case_info[i];
2360
- if (own_body.length === 0 && own_has_terminator) {
2361
- // `case 'a': break;` — exits the switch, then runs the tail.
2362
- return b.switch_case(original_case.test, [
2363
- combined_return_statement(render_nodes, tail_helper.component_element),
2364
- ]);
2365
- }
2366
- // Genuine empty fall-through (`case 'a': case 'b': ...`).
2367
- return b.switch_case(original_case.test, []);
2368
- },
2369
- );
2370
-
2371
- // Hoist all case helpers' setup statements above the switch in source
2372
- // order so the switch body is purely a dispatcher.
2373
- const case_helper_setup_statements = [];
2374
- for (const helper of case_helper_by_index) {
2375
- if (helper) case_helper_setup_statements.push(...helper.setup_statements);
2376
- }
2377
-
2378
- return [
2379
- ...tail_helper.setup_statements,
2380
- ...case_helper_setup_statements,
2381
- set_loc(b.switch(switch_node.discriminant, new_cases), switch_node),
2382
- combined_return_statement(render_nodes, tail_helper.component_element),
2383
- ];
2384
- }
2385
-
2386
- /**
2387
- * Hoist the helper for a hook-bearing for-of body out of the iteration
2388
- * callback so the helper is declared once per render rather than re-bound on
2389
- * every iteration. Loop-scoped param types are derived from the iteration
2390
- * source via a TS `type` alias (rather than the const+typeof pattern used
2391
- * for outer bindings, which would require the loop var to be in scope).
2392
- *
2393
- * The iteration source is hoisted into a generated `let` and normalized via
2394
- * `Array.isArray(src) ? src : Array.from(src)` so any Iterable / ArrayLike
2395
- * works while skipping the copy when the source is already an array. The
2396
- * iteration itself is emitted as `source.map((item, i) => ...)`.
2397
- *
2398
- * If `continuation_body` is non-empty (the for-of has a tail) we also lift
2399
- * the tail into a TailHelper and call it conditionally on the last iteration
2400
- * via an `isLast={i === source.length - 1}` prop on the loop helper. The
2401
- * loop helper's mutated locals (post-`useState`) flow into the TailHelper as
2402
- * its props. When the source is empty, `.map` returns `[]` and the TailHelper
2403
- * never renders — we add a sibling fallback so the source's tail still runs
2404
- * with the original outer values in that case.
2405
- *
2406
- * Bails out (returns null) when the loop pattern is destructured — deriving
2407
- * element types from a tuple/object pattern is more involved and deferred.
2408
- *
2409
- * @param {any} node - ForOfStatement
2410
- * @param {any[]} continuation_body
2411
- * @param {TransformContext} transform_context
2412
- * @returns {{ hoist_statements: any[], jsx_child: any } | null}
2413
- */
2414
- function build_hoisted_for_of_with_hooks(node, continuation_body, transform_context) {
2415
- const loop_params = get_for_of_iteration_params(node.left, node.index);
2416
- for (const param of loop_params) {
2417
- if (param.type !== 'Identifier') return null;
2418
- }
2419
-
2420
- const has_tail = continuation_body.length > 0;
2421
- const original_loop_body = /** @type {any[]} */ (
2422
- rewrite_loop_continues_to_bare_returns(
2423
- node.body.type === 'BlockStatement' ? node.body.body : [node.body],
2424
- )
2425
- );
2426
-
2427
- // When there's a tail, build TailHelper first so its component_element can
2428
- // be embedded inside the loop helper's body (gated on isLast). The
2429
- // synthetic isLast prop uses the loop helper's index (which will be the
2430
- // next one assigned, since `create_hook_safe_helper` for the tail just
2431
- // consumed one) so it lines up with `StatementBodyHook<N>` in the output.
2432
- let tail_helper = null;
2433
- /** @type {AST.Identifier} */ let tail_synthetic_id;
2434
- if (has_tail) {
2435
- tail_helper = build_tail_helper(continuation_body, node, transform_context);
2436
- tail_synthetic_id = create_generated_identifier(
2437
- `_tsrx_isLast_${transform_context.local_statement_component_index + 1}`,
2438
- );
2439
- } else {
2440
- tail_synthetic_id = /** @type {any} */ (null);
2441
- }
2442
- const loop_tail_expression = has_tail
2443
- ? create_loop_tail_expression(tail_synthetic_id, /** @type {any} */ (tail_helper))
2444
- : null;
2445
- const loop_body =
2446
- has_tail && loop_tail_expression
2447
- ? [...original_loop_body, b.jsx_expression_container(loop_tail_expression)]
2448
- : original_loop_body;
2449
-
2450
- const source_id = create_generated_identifier(
2451
- `_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
2452
- );
2453
- const { source_decl, source_normalize_decl } = build_array_normalization_decls(
2454
- source_id,
2455
- node.right,
2456
- );
2457
-
2458
- const saved_bindings = transform_context.available_bindings;
2459
- transform_context.available_bindings = new Map(saved_bindings);
2460
- const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
2461
- for (const param of loop_params) {
2462
- collect_pattern_bindings(param, transform_context.available_bindings);
2463
- }
2464
- validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
2465
- original_loop_body,
2466
- transform_context,
2467
- loop_scoped_names,
2468
- );
2469
-
2470
- const all_helper_bindings = get_referenced_helper_bindings(
2471
- loop_body,
2472
- transform_context.available_bindings,
2473
- );
2474
- const outer_bindings = all_helper_bindings.filter((b) => !loop_scoped_names.has(b.name));
2475
- const loop_bindings = all_helper_bindings.filter((b) => loop_scoped_names.has(b.name));
2476
-
2477
- const helper_id = create_generated_identifier(
2478
- create_local_statement_component_name(transform_context),
2479
- );
2480
- const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
2481
- const component_id = use_module_scoped_component
2482
- ? create_module_scoped_hook_component_id(helper_id, transform_context)
2483
- : helper_id;
1933
+ const helper_id = create_generated_identifier(
1934
+ create_local_statement_component_name(transform_context),
1935
+ );
1936
+ const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
1937
+ const component_id = use_module_scoped_component
1938
+ ? create_module_scoped_hook_component_id(helper_id, transform_context)
1939
+ : helper_id;
2484
1940
 
2485
1941
  const outer_aliases = use_module_scoped_component
2486
1942
  ? []
@@ -2488,69 +1944,42 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2488
1944
  const loop_aliases = use_module_scoped_component
2489
1945
  ? []
2490
1946
  : loop_bindings.map((binding) =>
2491
- create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params),
1947
+ create_loop_scoped_type_alias_declaration(
1948
+ helper_id,
1949
+ binding,
1950
+ source_id,
1951
+ loop_params,
1952
+ transform_context,
1953
+ ),
2492
1954
  );
2493
1955
 
2494
- // Synthetic `isLast` prop on the loop helper when there's a tail. It's
2495
- // passed from the .map callback as `i === source.length - 1` so every
2496
- // loop-helper return can append the tail helper on the last iteration.
2497
- const tail_isLast_alias = has_tail
2498
- ? use_module_scoped_component
2499
- ? null
2500
- : {
2501
- id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2502
- declaration: b.ts_type_alias(
2503
- create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2504
- b.ts_keyword_type('boolean'),
2505
- ),
2506
- }
2507
- : null;
2508
-
2509
1956
  const ordered_bindings = [...outer_bindings, ...loop_bindings];
2510
1957
  const ordered_aliases = [...outer_aliases, ...loop_aliases];
2511
1958
  const ordered_use_typeof = [...outer_bindings.map(() => true), ...loop_bindings.map(() => false)];
2512
1959
 
2513
- const signature_bindings = has_tail ? [...ordered_bindings, tail_synthetic_id] : ordered_bindings;
2514
- const signature_aliases = has_tail
2515
- ? [...ordered_aliases, /** @type {any} */ (tail_isLast_alias)]
2516
- : ordered_aliases;
2517
- const signature_use_typeof = has_tail ? [...ordered_use_typeof, false] : ordered_use_typeof;
2518
-
2519
1960
  const props_type =
2520
- signature_bindings.length > 0 && !use_module_scoped_component
1961
+ ordered_bindings.length > 0 && !use_module_scoped_component
2521
1962
  ? create_helper_props_type_literal_with_typeof_flags(
2522
- signature_bindings,
2523
- signature_aliases,
2524
- signature_use_typeof,
1963
+ ordered_bindings,
1964
+ ordered_aliases,
1965
+ ordered_use_typeof,
2525
1966
  )
2526
1967
  : null;
2527
1968
  const params =
2528
- signature_bindings.length > 0
1969
+ ordered_bindings.length > 0
2529
1970
  ? [
2530
1971
  props_type !== null
2531
- ? create_typed_helper_props_pattern(signature_bindings, props_type)
2532
- : create_helper_props_pattern(signature_bindings),
1972
+ ? create_typed_helper_props_pattern(ordered_bindings, props_type)
1973
+ : create_helper_props_pattern(ordered_bindings),
2533
1974
  ]
2534
1975
  : [];
2535
1976
 
2536
1977
  const fn_saved_bindings = transform_context.available_bindings;
2537
1978
  transform_context.available_bindings = new Map(fn_saved_bindings);
2538
- if (has_tail) {
2539
- transform_context.available_bindings.set(tail_synthetic_id.name, tail_synthetic_id);
2540
- }
2541
- const fn_body_statements = build_render_statements(loop_body, true, transform_context);
2542
- if (has_tail) {
2543
- append_loop_tail_to_return_statements(
2544
- fn_body_statements,
2545
- tail_synthetic_id,
2546
- /** @type {any} */ (tail_helper),
2547
- );
2548
- }
1979
+ const fn_body_statements = build_render_statements(original_loop_body, true, transform_context);
2549
1980
  transform_context.available_bindings = fn_saved_bindings;
2550
1981
 
2551
- const helper_fn = /** @type {any} */ (
2552
- b.function(clone_identifier(component_id), params, b.block(fn_body_statements))
2553
- );
1982
+ const helper_fn = b.function(clone_identifier(component_id), params, b.block(fn_body_statements));
2554
1983
  helper_fn.metadata = { path: [], is_component: true, is_method: false };
2555
1984
 
2556
1985
  let helper_decl;
@@ -2582,18 +2011,6 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2582
2011
  { mapWrapper: false, mapBindingNames: false, mapBindingValues: false },
2583
2012
  );
2584
2013
 
2585
- // When there's a tail, the .map callback always needs an index to compute
2586
- // `isLast`. If the user didn't write `index i`, synthesize one. The same
2587
- // identifier is also used as the implicit key fallback below.
2588
- let index_identifier;
2589
- if (loop_params.length >= 2) {
2590
- index_identifier = clone_identifier(loop_params[1]);
2591
- } else if (has_tail) {
2592
- index_identifier = create_generated_identifier('i');
2593
- } else {
2594
- index_identifier = null;
2595
- }
2596
-
2597
2014
  const body_key_expression = find_key_expression_in_body(original_loop_body);
2598
2015
  const explicit_key_expression =
2599
2016
  body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
@@ -2606,57 +2023,24 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2606
2023
  );
2607
2024
  }
2608
2025
 
2609
- if (has_tail && index_identifier) {
2610
- const length_minus_one = b.binary(
2611
- '-',
2612
- b.member(clone_identifier(source_id), 'length'),
2613
- b.literal(1),
2614
- );
2615
- callback_invocation_element.openingElement.attributes.push(
2616
- b.jsx_attribute(
2617
- b.jsx_id(tail_synthetic_id.name),
2618
- to_jsx_expression_container(
2619
- b.binary('===', clone_identifier(index_identifier), length_minus_one),
2620
- ),
2621
- ),
2622
- );
2623
- }
2624
-
2625
- const callback_params =
2626
- has_tail && loop_params.length < 2 && index_identifier
2627
- ? [
2628
- ...loop_params.map((/** @type {any} */ p) => clone_identifier(p)),
2629
- clone_identifier(index_identifier),
2630
- ]
2631
- : loop_params.map((/** @type {any} */ p) => clone_identifier(p));
2026
+ const callback_params = loop_params.map((/** @type {any} */ p) => clone_identifier(p));
2632
2027
 
2633
2028
  const iter_callback = b.arrow(callback_params, callback_invocation_element);
2634
2029
 
2635
- const map_call = b.call(b.member(clone_identifier(source_id), 'map'), iter_callback);
2636
-
2637
- // jsx_child for the iteration. When there's a tail, also render the tail
2638
- // helper directly when the source is empty (no iterations means the loop
2639
- // helper never fires, so the tail wouldn't run otherwise).
2640
- const jsx_child = has_tail
2641
- ? to_jsx_expression_container(
2642
- b.conditional(
2643
- b.binary('===', b.member(clone_identifier(source_id), 'length'), b.literal(0)),
2644
- clone_tail_invocation(/** @type {any} */ (tail_helper)),
2645
- map_call,
2646
- ),
2647
- node,
2648
- )
2649
- : to_jsx_expression_container(map_call, node);
2650
-
2651
- const hoist_statements = [source_decl, source_normalize_decl];
2652
- if (has_tail) {
2653
- // TailHelper's setup statements (its alias consts and cache decl).
2654
- hoist_statements.push(.../** @type {any} */ (tail_helper).setup_statements);
2030
+ let map_call;
2031
+ if (use_iterable_helper) {
2032
+ transform_context.needs_for_of_iterable = true;
2033
+ map_call = b.call(b.id(MAP_ITERABLE_INTERNAL_NAME), clone_identifier(source_id), iter_callback);
2034
+ } else {
2035
+ map_call = b.call(b.member(clone_identifier(source_id), 'map'), iter_callback);
2655
2036
  }
2037
+
2038
+ const jsx_child = to_jsx_expression_container(map_call, node);
2039
+
2040
+ const hoist_statements = source_normalize_decl
2041
+ ? [source_decl, source_normalize_decl]
2042
+ : [source_decl];
2656
2043
  for (const alias of ordered_aliases) hoist_statements.push(alias.declaration);
2657
- if (has_tail && tail_isLast_alias) {
2658
- hoist_statements.push(tail_isLast_alias.declaration);
2659
- }
2660
2044
  if (helper_decl) {
2661
2045
  hoist_statements.push(helper_decl);
2662
2046
  }
@@ -2669,28 +2053,49 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2669
2053
 
2670
2054
  /**
2671
2055
  * Build a TS `type` alias for a loop-scoped binding, deriving the type
2672
- * from the iteration source. For the value param we use
2673
- * `(typeof source)[number]`, which gives the right element type for arrays
2674
- * and tuples (the common case in JSX templates). For the index param,
2675
- * the type is always `number`.
2056
+ * from the iteration source. For the index param the type is always
2057
+ * `number`. For the value param the shape depends on whether the platform
2058
+ * uses the `map_iterable` runtime helper:
2059
+ *
2060
+ * - With the helper (React, Preact): `IterationValue<typeof source>` — any
2061
+ * `Iterable<T>` is accepted, so the element type is derived through the
2062
+ * runtime's exported helper type.
2063
+ * - Without the helper: `(typeof source)[number]` — arrays/tuples only,
2064
+ * matching the inline `.map()` lowering.
2676
2065
  *
2677
2066
  * @param {AST.Identifier} helper_id
2678
2067
  * @param {AST.Identifier} binding
2679
2068
  * @param {AST.Identifier} source_id
2680
2069
  * @param {any[]} loop_params
2070
+ * @param {TransformContext} transform_context
2681
2071
  * @returns {{ id: AST.Identifier, declaration: any }}
2682
2072
  */
2683
- function create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params) {
2073
+ function create_loop_scoped_type_alias_declaration(
2074
+ helper_id,
2075
+ binding,
2076
+ source_id,
2077
+ loop_params,
2078
+ transform_context,
2079
+ ) {
2684
2080
  const alias_id = create_generated_identifier(`_tsrx_${helper_id.name}_${binding.name}`);
2685
2081
  const is_index = loop_params.length > 1 && binding.name === loop_params[1].name;
2082
+ const use_iterable_helper = !!transform_context.platform.imports.forOfIterableHelper;
2686
2083
  const type_annotation = is_index
2687
2084
  ? b.ts_keyword_type('number')
2688
- : /** @type {any} */ ({
2689
- type: 'TSIndexedAccessType',
2690
- objectType: b.ts_type_query(clone_identifier(source_id)),
2691
- indexType: b.ts_keyword_type('number'),
2692
- metadata: { path: [] },
2693
- });
2085
+ : use_iterable_helper
2086
+ ? (() => {
2087
+ transform_context.needs_iteration_value_type = true;
2088
+ return b.ts_type_reference(
2089
+ b.id(ITERATION_VALUE_INTERNAL_NAME),
2090
+ b.ts_type_parameter_instantiation([b.ts_type_query(clone_identifier(source_id))]),
2091
+ );
2092
+ })()
2093
+ : /** @type {any} */ ({
2094
+ type: 'TSIndexedAccessType',
2095
+ objectType: b.ts_type_query(clone_identifier(source_id)),
2096
+ indexType: b.ts_keyword_type('number'),
2097
+ metadata: { path: [] },
2098
+ });
2694
2099
 
2695
2100
  return {
2696
2101
  id: alias_id,
@@ -2750,23 +2155,19 @@ function create_setup_once_helper_split_returning_if_statements(
2750
2155
  return [
2751
2156
  ...branch_helper.setup_statements,
2752
2157
  ...continuation_helper.setup_statements,
2753
- {
2754
- type: 'ReturnStatement',
2755
- argument: combine_render_return_argument(
2158
+ b.return(
2159
+ combine_render_return_argument(
2756
2160
  render_nodes,
2757
2161
  set_loc(
2758
- /** @type {any} */ ({
2759
- type: 'ConditionalExpression',
2760
- test: node.test,
2761
- consequent: branch_helper.component_element,
2762
- alternate: continuation_helper.component_element,
2763
- metadata: { path: [] },
2764
- }),
2162
+ b.conditional(
2163
+ node.test,
2164
+ branch_helper.component_element,
2165
+ continuation_helper.component_element,
2166
+ ),
2765
2167
  node,
2766
2168
  ),
2767
2169
  ),
2768
- metadata: { path: [] },
2769
- },
2170
+ ),
2770
2171
  ];
2771
2172
  }
2772
2173
 
@@ -3038,25 +2439,7 @@ function statement_body_to_jsx_child(body_nodes, transform_context) {
3038
2439
  }
3039
2440
 
3040
2441
  return to_jsx_expression_container(
3041
- /** @type {any} */ ({
3042
- type: 'CallExpression',
3043
- callee: {
3044
- type: 'ArrowFunctionExpression',
3045
- params: [],
3046
- body: /** @type {any} */ ({
3047
- type: 'BlockStatement',
3048
- body: build_render_statements(body_nodes, true, transform_context),
3049
- metadata: { path: [] },
3050
- }),
3051
- async: false,
3052
- generator: false,
3053
- expression: false,
3054
- metadata: { path: [] },
3055
- },
3056
- arguments: [],
3057
- optional: false,
3058
- metadata: { path: [] },
3059
- }),
2442
+ b.call(b.arrow([], b.block(build_render_statements(body_nodes, true, transform_context)))),
3060
2443
  );
3061
2444
  }
3062
2445
 
@@ -3107,11 +2490,7 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
3107
2490
  );
3108
2491
  const statements = [...helper.setup_statements];
3109
2492
 
3110
- statements.push({
3111
- type: 'ReturnStatement',
3112
- argument: helper.component_element,
3113
- metadata: { path: [] },
3114
- });
2493
+ statements.push(b.return(helper.component_element));
3115
2494
 
3116
2495
  return statements;
3117
2496
  }
@@ -3307,7 +2686,7 @@ function validate_hook_outer_assignments_in_node(
3307
2686
  return;
3308
2687
  }
3309
2688
 
3310
- if (is_function_like_node(node)) {
2689
+ if (is_function_or_component_node(node)) {
3311
2690
  return;
3312
2691
  }
3313
2692
 
@@ -3365,7 +2744,7 @@ function validate_hook_outer_assignments_in_node(
3365
2744
  );
3366
2745
  if (outer_names.length > 0) {
3367
2746
  report_hook_outer_assignment_error(
3368
- node,
2747
+ node.left,
3369
2748
  outer_names,
3370
2749
  find_first_hook_call_name(node.right) || 'hook',
3371
2750
  transform_context,
@@ -3389,7 +2768,7 @@ function validate_hook_outer_assignments_in_node(
3389
2768
  );
3390
2769
  if (outer_names.length > 0) {
3391
2770
  report_hook_outer_assignment_error(
3392
- node,
2771
+ node.left,
3393
2772
  outer_names,
3394
2773
  find_first_hook_call_name(node.right) || 'hook',
3395
2774
  transform_context,
@@ -3449,7 +2828,7 @@ function validate_hook_outer_assignments_in_node(
3449
2828
  function validate_hook_callback_outer_mutations(call_node, shadowed_names, transform_context) {
3450
2829
  const hook_name = get_hook_callee_name(call_node.callee);
3451
2830
  for (const argument of call_node.arguments || []) {
3452
- if (!is_function_like_node(argument)) {
2831
+ if (!is_function_or_component_node(argument)) {
3453
2832
  continue;
3454
2833
  }
3455
2834
  const callback_shadowed_names = create_function_like_shadowed_names(argument, shadowed_names);
@@ -3462,21 +2841,6 @@ function validate_hook_callback_outer_mutations(call_node, shadowed_names, trans
3462
2841
  }
3463
2842
  }
3464
2843
 
3465
- /**
3466
- * @param {any} node
3467
- * @returns {boolean}
3468
- */
3469
- function is_function_like_node(node) {
3470
- return (
3471
- node.type === 'FunctionDeclaration' ||
3472
- node.type === 'FunctionExpression' ||
3473
- node.type === 'ArrowFunctionExpression' ||
3474
- // this is just in case but we should already
3475
- // have a component replaced with a function node
3476
- node.type === 'Component'
3477
- );
3478
- }
3479
-
3480
2844
  /**
3481
2845
  * @param {any} node
3482
2846
  * @param {Set<string>} shadowed_names
@@ -3524,7 +2888,7 @@ function validate_hook_callback_outer_mutations_in_node(
3524
2888
  return;
3525
2889
  }
3526
2890
 
3527
- if (is_function_like_node(node)) {
2891
+ if (is_function_or_component_node(node)) {
3528
2892
  validate_hook_callback_outer_mutations_in_node(
3529
2893
  node.body,
3530
2894
  create_function_like_shadowed_names(node, shadowed_names),
@@ -3557,7 +2921,12 @@ function validate_hook_callback_outer_mutations_in_node(
3557
2921
  shadowed_names,
3558
2922
  );
3559
2923
  if (outer_names.length > 0) {
3560
- report_hook_callback_outer_mutation_error(node, outer_names, hook_name, transform_context);
2924
+ report_hook_callback_outer_mutation_error(
2925
+ node.left,
2926
+ outer_names,
2927
+ hook_name,
2928
+ transform_context,
2929
+ );
3561
2930
  }
3562
2931
  }
3563
2932
 
@@ -3568,7 +2937,12 @@ function validate_hook_callback_outer_mutations_in_node(
3568
2937
  shadowed_names,
3569
2938
  );
3570
2939
  if (outer_names.length > 0) {
3571
- report_hook_callback_outer_mutation_error(node, outer_names, hook_name, transform_context);
2940
+ report_hook_callback_outer_mutation_error(
2941
+ node.argument,
2942
+ outer_names,
2943
+ hook_name,
2944
+ transform_context,
2945
+ );
3572
2946
  }
3573
2947
  }
3574
2948
 
@@ -3863,23 +3237,13 @@ function create_hook_safe_helper(
3863
3237
  const saved_bindings = transform_context.available_bindings;
3864
3238
  transform_context.available_bindings = new Map(saved_bindings);
3865
3239
 
3866
- const helper_fn = /** @type {any} */ ({
3867
- type: 'FunctionExpression',
3868
- id: clone_identifier(component_id),
3240
+ const helper_fn = b.function(
3241
+ clone_identifier(component_id),
3869
3242
  params,
3870
- body: {
3871
- type: 'BlockStatement',
3872
- body: build_render_statements(body_nodes, true, transform_context),
3873
- metadata: { path: [] },
3874
- },
3875
- async: false,
3876
- generator: false,
3877
- metadata: {
3878
- path: [],
3879
- is_component: true,
3880
- is_method: false,
3881
- },
3882
- });
3243
+ b.block(build_render_statements(body_nodes, true, transform_context)),
3244
+ );
3245
+ helper_fn.metadata.is_component = true;
3246
+ helper_fn.metadata.is_method = false;
3883
3247
 
3884
3248
  transform_context.available_bindings = saved_bindings;
3885
3249
 
@@ -3945,7 +3309,7 @@ function create_hook_safe_helper(
3945
3309
 
3946
3310
  /**
3947
3311
  * @param {AST.Identifier} helper_id
3948
- * @param {any} helper_fn
3312
+ * @param {AST.FunctionExpression} helper_fn
3949
3313
  * @param {any} source_node
3950
3314
  * @param {TransformContext} transform_context
3951
3315
  * @returns {any}
@@ -3958,7 +3322,7 @@ function create_helper_declaration(helper_id, helper_fn, source_node, transform_
3958
3322
 
3959
3323
  /**
3960
3324
  * @param {AST.Identifier} helper_id
3961
- * @param {any} helper_fn
3325
+ * @param {AST.FunctionExpression} helper_fn
3962
3326
  * @param {any} source_node
3963
3327
  * @param {TransformContext} transform_context
3964
3328
  * @returns {any}
@@ -3987,32 +3351,7 @@ function create_helper_init_expression(helper_id, helper_fn, source_node, transf
3987
3351
  * @returns {any}
3988
3352
  */
3989
3353
  function create_hook_safe_helper_iife(setup_statements, component_element) {
3990
- return /** @type {any} */ ({
3991
- type: 'CallExpression',
3992
- callee: {
3993
- type: 'ArrowFunctionExpression',
3994
- params: [],
3995
- body: /** @type {any} */ ({
3996
- type: 'BlockStatement',
3997
- body: [
3998
- ...setup_statements,
3999
- {
4000
- type: 'ReturnStatement',
4001
- argument: component_element,
4002
- metadata: { path: [] },
4003
- },
4004
- ],
4005
- metadata: { path: [] },
4006
- }),
4007
- async: false,
4008
- generator: false,
4009
- expression: false,
4010
- metadata: { path: [] },
4011
- },
4012
- arguments: [],
4013
- optional: false,
4014
- metadata: { path: [] },
4015
- });
3354
+ return b.call(b.arrow([], b.block([...setup_statements, b.return(component_element)])));
4016
3355
  }
4017
3356
 
4018
3357
  /**
@@ -4025,19 +3364,7 @@ function create_helper_type_alias_declaration(helper_id, binding) {
4025
3364
 
4026
3365
  return {
4027
3366
  id: alias_id,
4028
- declaration: /** @type {any} */ ({
4029
- type: 'VariableDeclaration',
4030
- kind: 'const',
4031
- declarations: [
4032
- {
4033
- type: 'VariableDeclarator',
4034
- id: clone_identifier(alias_id),
4035
- init: create_generated_identifier(binding.name),
4036
- metadata: { path: [] },
4037
- },
4038
- ],
4039
- metadata: { path: [] },
4040
- }),
3367
+ declaration: b.const(clone_identifier(alias_id), create_generated_identifier(binding.name)),
4041
3368
  };
4042
3369
  }
4043
3370
 
@@ -4047,33 +3374,14 @@ function create_helper_type_alias_declaration(helper_id, binding) {
4047
3374
  * @returns {any}
4048
3375
  */
4049
3376
  function create_helper_props_type_literal(bindings, aliases) {
4050
- return /** @type {any} */ ({
4051
- type: 'TSTypeLiteral',
4052
- members: bindings.map(
4053
- (binding, i) =>
4054
- /** @type {any} */ ({
4055
- type: 'TSPropertySignature',
4056
- key: create_generated_identifier(binding.name),
4057
- computed: false,
4058
- optional: false,
4059
- readonly: false,
4060
- static: false,
4061
- kind: 'init',
4062
- typeAnnotation: {
4063
- type: 'TSTypeAnnotation',
4064
- typeAnnotation: {
4065
- type: 'TSTypeQuery',
4066
- exprName: clone_identifier(aliases[i].id),
4067
- typeArguments: null,
4068
- metadata: { path: [] },
4069
- },
4070
- metadata: { path: [] },
4071
- },
4072
- metadata: { path: [] },
4073
- }),
3377
+ return b.ts_type_literal(
3378
+ bindings.map((binding, i) =>
3379
+ b.ts_property_signature(
3380
+ create_generated_identifier(binding.name),
3381
+ b.ts_type_annotation(b.ts_type_query(clone_identifier(aliases[i].id))),
3382
+ ),
4074
3383
  ),
4075
- metadata: { path: [] },
4076
- });
3384
+ );
4077
3385
  }
4078
3386
 
4079
3387
  /**
@@ -4083,11 +3391,7 @@ function create_helper_props_type_literal(bindings, aliases) {
4083
3391
  */
4084
3392
  function create_typed_helper_props_pattern(bindings, props_type) {
4085
3393
  const pattern = create_helper_props_pattern(bindings);
4086
- /** @type {any} */ (pattern).typeAnnotation = {
4087
- type: 'TSTypeAnnotation',
4088
- typeAnnotation: props_type,
4089
- metadata: { path: [] },
4090
- };
3394
+ /** @type {any} */ (pattern).typeAnnotation = b.ts_type_annotation(props_type);
4091
3395
  return pattern;
4092
3396
  }
4093
3397
 
@@ -4096,19 +3400,7 @@ function create_typed_helper_props_pattern(bindings, props_type) {
4096
3400
  * @returns {any}
4097
3401
  */
4098
3402
  function create_helper_cache_declaration(cache_id) {
4099
- return /** @type {any} */ ({
4100
- type: 'VariableDeclaration',
4101
- kind: 'let',
4102
- declarations: [
4103
- {
4104
- type: 'VariableDeclarator',
4105
- id: clone_identifier(cache_id),
4106
- init: null,
4107
- metadata: { path: [] },
4108
- },
4109
- ],
4110
- metadata: { path: [] },
4111
- });
3403
+ return b.let(clone_identifier(cache_id));
4112
3404
  }
4113
3405
 
4114
3406
  /**
@@ -4118,44 +3410,27 @@ function create_helper_cache_declaration(cache_id) {
4118
3410
  * @returns {any}
4119
3411
  */
4120
3412
  function create_cached_helper_declaration(helper_id, cache_id, helper_init) {
4121
- return /** @type {any} */ ({
4122
- type: 'VariableDeclaration',
4123
- kind: 'const',
4124
- declarations: [
4125
- {
4126
- type: 'VariableDeclarator',
4127
- id: clone_identifier(helper_id),
4128
- init: {
4129
- type: 'LogicalExpression',
4130
- operator: '??',
4131
- left: clone_identifier(cache_id),
4132
- right: {
4133
- type: 'AssignmentExpression',
4134
- operator: '=',
4135
- left: clone_identifier(cache_id),
4136
- right: helper_init,
4137
- metadata: { path: [] },
4138
- },
4139
- metadata: { path: [] },
4140
- },
4141
- metadata: { path: [] },
4142
- },
4143
- ],
4144
- metadata: { path: [] },
4145
- });
3413
+ return b.const(
3414
+ clone_identifier(helper_id),
3415
+ b.logical(
3416
+ '??',
3417
+ clone_identifier(cache_id),
3418
+ b.assignment('=', clone_identifier(cache_id), helper_init),
3419
+ ),
3420
+ );
4146
3421
  }
4147
3422
 
4148
3423
  /**
4149
3424
  * @param {AST.Identifier} helper_id
4150
- * @param {any} helper_fn
3425
+ * @param {AST.FunctionExpression} helper_fn
4151
3426
  * @returns {AST.FunctionDeclaration}
4152
3427
  */
4153
3428
  function create_helper_function_declaration_from_expression(helper_id, helper_fn) {
4154
- return /** @type {any} */ ({
3429
+ return {
4155
3430
  ...helper_fn,
4156
3431
  type: 'FunctionDeclaration',
4157
3432
  id: clone_identifier(helper_id),
4158
- });
3433
+ };
4159
3434
  }
4160
3435
 
4161
3436
  /**
@@ -4540,25 +3815,7 @@ function if_statement_to_jsx_child(node, transform_context) {
4540
3815
  }
4541
3816
 
4542
3817
  return to_jsx_expression_container(
4543
- /** @type {any} */ ({
4544
- type: 'CallExpression',
4545
- callee: {
4546
- type: 'ArrowFunctionExpression',
4547
- params: [],
4548
- body: /** @type {any} */ ({
4549
- type: 'BlockStatement',
4550
- body: [render_if_statement, create_null_return_statement()],
4551
- metadata: { path: [] },
4552
- }),
4553
- async: false,
4554
- generator: false,
4555
- expression: false,
4556
- metadata: { path: [] },
4557
- },
4558
- arguments: [],
4559
- optional: false,
4560
- metadata: { path: [] },
4561
- }),
3818
+ b.call(b.arrow([], b.block([render_if_statement, create_null_return_statement()]))),
4562
3819
  );
4563
3820
  }
4564
3821
 
@@ -4583,16 +3840,7 @@ function render_if_statement_to_conditional_expression(node) {
4583
3840
  }
4584
3841
  }
4585
3842
 
4586
- return set_loc(
4587
- /** @type {any} */ ({
4588
- type: 'ConditionalExpression',
4589
- test: node.test,
4590
- consequent,
4591
- alternate,
4592
- metadata: { path: [] },
4593
- }),
4594
- node,
4595
- );
3843
+ return set_loc(b.conditional(node.test, consequent, alternate), node);
4596
3844
  }
4597
3845
 
4598
3846
  /**
@@ -4663,14 +3911,7 @@ function find_key_expression_in_body(body_nodes) {
4663
3911
  * @returns {any}
4664
3912
  */
4665
3913
  function continue_to_bare_return(source_node) {
4666
- return set_loc(
4667
- /** @type {any} */ ({
4668
- type: 'ReturnStatement',
4669
- argument: null,
4670
- metadata: { path: [] },
4671
- }),
4672
- source_node,
4673
- );
3914
+ return set_loc(b.return(null), source_node);
4674
3915
  }
4675
3916
 
4676
3917
  /**
@@ -4796,36 +4037,17 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4796
4037
  // Restore bindings
4797
4038
  transform_context.available_bindings = saved_bindings;
4798
4039
 
4040
+ const iter_callback = b.arrow(loop_params, b.block(body_statements));
4041
+
4042
+ if (transform_context.platform.imports.forOfIterableHelper) {
4043
+ transform_context.needs_for_of_iterable = true;
4044
+ return to_jsx_expression_container(
4045
+ b.call(b.id(MAP_ITERABLE_INTERNAL_NAME), node.right, iter_callback),
4046
+ );
4047
+ }
4048
+
4799
4049
  return to_jsx_expression_container(
4800
- /** @type {any} */ ({
4801
- type: 'CallExpression',
4802
- callee: {
4803
- type: 'MemberExpression',
4804
- object: node.right,
4805
- property: create_generated_identifier('map'),
4806
- computed: false,
4807
- optional: false,
4808
- metadata: { path: [] },
4809
- },
4810
- arguments: [
4811
- {
4812
- type: 'ArrowFunctionExpression',
4813
- params: loop_params,
4814
- body: /** @type {any} */ ({
4815
- type: 'BlockStatement',
4816
- body: body_statements,
4817
- metadata: { path: [] },
4818
- }),
4819
- async: false,
4820
- generator: false,
4821
- expression: false,
4822
- metadata: { path: [] },
4823
- },
4824
- ],
4825
- async: false,
4826
- optional: false,
4827
- metadata: { path: [] },
4828
- }),
4050
+ b.call(b.member(node.right, create_generated_identifier('map')), iter_callback),
4829
4051
  );
4830
4052
  }
4831
4053
 
@@ -4846,7 +4068,7 @@ function apply_key_to_loop_body(body_nodes, key_expression) {
4846
4068
  if (!has_key) {
4847
4069
  attributes.push({
4848
4070
  type: 'Attribute',
4849
- name: { type: 'Identifier', name: 'key', metadata: { path: [] } },
4071
+ name: b.id('key'),
4850
4072
  value: clone_expression_node(key_expression),
4851
4073
  shorthand: false,
4852
4074
  metadata: { path: [] },
@@ -4866,15 +4088,10 @@ function apply_key_to_loop_body(body_nodes, key_expression) {
4866
4088
 
4867
4089
  if (!has_key) {
4868
4090
  attributes.push(
4869
- /** @type {any} */ ({
4870
- type: 'JSXAttribute',
4871
- name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
4872
- value: to_jsx_expression_container(
4873
- clone_expression_node(key_expression),
4874
- key_expression,
4875
- ),
4876
- metadata: { path: [] },
4877
- }),
4091
+ b.jsx_attribute(
4092
+ b.jsx_id('key'),
4093
+ to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
4094
+ ),
4878
4095
  );
4879
4096
  }
4880
4097
  return;
@@ -4969,29 +4186,12 @@ function keyed_fragment_to_jsx_element(fragment, key_expression) {
4969
4186
  * @returns {ESTreeJSX.JSXExpressionContainer}
4970
4187
  */
4971
4188
  function switch_statement_to_jsx_child(node, transform_context) {
4189
+ const { setup_statements, switch_statement } = build_switch_with_lift(node, transform_context);
4190
+
4972
4191
  return to_jsx_expression_container(
4973
- /** @type {any} */ ({
4974
- type: 'CallExpression',
4975
- callee: {
4976
- type: 'ArrowFunctionExpression',
4977
- params: [],
4978
- body: /** @type {any} */ ({
4979
- type: 'BlockStatement',
4980
- body: [
4981
- create_render_switch_statement(node, transform_context),
4982
- create_null_return_statement(),
4983
- ],
4984
- metadata: { path: [] },
4985
- }),
4986
- async: false,
4987
- generator: false,
4988
- expression: false,
4989
- metadata: { path: [] },
4990
- },
4991
- arguments: [],
4992
- optional: false,
4993
- metadata: { path: [] },
4994
- }),
4192
+ b.call(
4193
+ b.arrow([], b.block([...setup_statements, switch_statement, create_null_return_statement()])),
4194
+ ),
4995
4195
  );
4996
4196
  }
4997
4197
 
@@ -5131,19 +4331,10 @@ function try_statement_to_jsx_child(node, transform_context) {
5131
4331
  catch_scoped_names,
5132
4332
  );
5133
4333
 
5134
- const fallback_fn = {
5135
- type: 'ArrowFunctionExpression',
5136
- params: catch_params,
5137
- body: /** @type {any} */ ({
5138
- type: 'BlockStatement',
5139
- body: build_render_statements(catch_body_nodes, true, transform_context),
5140
- metadata: { path: [] },
5141
- }),
5142
- async: false,
5143
- generator: false,
5144
- expression: false,
5145
- metadata: { path: [] },
5146
- };
4334
+ const fallback_fn = b.arrow(
4335
+ catch_params,
4336
+ b.block(build_render_statements(catch_body_nodes, true, transform_context)),
4337
+ );
5147
4338
 
5148
4339
  transform_context.available_bindings = saved_catch_bindings;
5149
4340
 
@@ -5156,40 +4347,10 @@ function try_statement_to_jsx_child(node, transform_context) {
5156
4347
 
5157
4348
  if (boundary_content && transform_context.inside_element_child) {
5158
4349
  result = to_jsx_expression_container(
5159
- /** @type {any} */ ({
5160
- type: 'CallExpression',
5161
- callee: { type: 'Identifier', name: 'TsrxErrorBoundary', metadata: { path: [] } },
5162
- arguments: [
5163
- {
5164
- type: 'ObjectExpression',
5165
- properties: [
5166
- {
5167
- type: 'Property',
5168
- key: { type: 'Identifier', name: 'fallback', metadata: { path: [] } },
5169
- value: fallback_fn,
5170
- kind: 'init',
5171
- method: false,
5172
- shorthand: false,
5173
- computed: false,
5174
- metadata: { path: [] },
5175
- },
5176
- {
5177
- type: 'Property',
5178
- key: { type: 'Identifier', name: 'content', metadata: { path: [] } },
5179
- value: boundary_content,
5180
- kind: 'init',
5181
- method: false,
5182
- shorthand: false,
5183
- computed: false,
5184
- metadata: { path: [] },
5185
- },
5186
- ],
5187
- metadata: { path: [] },
5188
- },
5189
- ],
5190
- optional: false,
5191
- metadata: { path: [] },
5192
- }),
4350
+ b.call(
4351
+ 'TsrxErrorBoundary',
4352
+ b.object([b.init('fallback', fallback_fn), b.init('content', boundary_content)]),
4353
+ ),
5193
4354
  );
5194
4355
 
5195
4356
  return result;
@@ -5198,21 +4359,12 @@ function try_statement_to_jsx_child(node, transform_context) {
5198
4359
  result = create_jsx_element(
5199
4360
  'TsrxErrorBoundary',
5200
4361
  [
5201
- {
5202
- type: 'JSXAttribute',
5203
- name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
5204
- value: to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
5205
- metadata: { path: [] },
5206
- },
4362
+ b.jsx_attribute(
4363
+ b.jsx_id('fallback'),
4364
+ to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
4365
+ ),
5207
4366
  ...(boundary_content
5208
- ? [
5209
- {
5210
- type: 'JSXAttribute',
5211
- name: { type: 'JSXIdentifier', name: 'content', metadata: { path: [] } },
5212
- value: to_jsx_expression_container(boundary_content),
5213
- metadata: { path: [] },
5214
- },
5215
- ]
4367
+ ? [b.jsx_attribute(b.jsx_id('content'), to_jsx_expression_container(boundary_content))]
5216
4368
  : []),
5217
4369
  ],
5218
4370
  boundary_content ? [] : [result],
@@ -5261,73 +4413,29 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
5261
4413
  const imports = [];
5262
4414
 
5263
4415
  if (transform_context.needs_fragment && platform.imports.fragment) {
5264
- const fragment_source = platform.imports.fragment;
5265
- imports.push({
5266
- type: 'ImportDeclaration',
5267
- specifiers: [
5268
- {
5269
- type: 'ImportSpecifier',
5270
- imported: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
5271
- local: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
5272
- metadata: { path: [] },
5273
- },
5274
- ],
5275
- source: {
5276
- type: 'Literal',
5277
- value: fragment_source,
5278
- raw: `'${fragment_source}'`,
5279
- },
5280
- metadata: { path: [] },
5281
- });
4416
+ imports.push(b.imports([['Fragment', 'Fragment']], platform.imports.fragment));
5282
4417
  }
5283
4418
 
5284
4419
  if (transform_context.needs_suspense) {
5285
- imports.push({
5286
- type: 'ImportDeclaration',
5287
- specifiers: [
5288
- {
5289
- type: 'ImportSpecifier',
5290
- imported: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
5291
- local: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
5292
- metadata: { path: [] },
5293
- },
5294
- ],
5295
- source: {
5296
- type: 'Literal',
5297
- value: suspense_source,
5298
- raw: `'${suspense_source}'`,
5299
- },
5300
- metadata: { path: [] },
5301
- });
4420
+ imports.push(b.imports([['Suspense', 'Suspense']], suspense_source));
4421
+ }
4422
+
4423
+ if (transform_context.needs_for_of_iterable && platform.imports.forOfIterableHelper) {
4424
+ const specifiers = [b.import_specifier('map_iterable', MAP_ITERABLE_INTERNAL_NAME)];
4425
+ // The loop-scoped type alias `IterationValue<typeof source>` only
4426
+ // appears in the output when at least one hook-bearing for-of body
4427
+ // was lowered with non-module-scoped helpers (editor tooling sets
4428
+ // this for typeOnly virtual modules).
4429
+ if (transform_context.needs_iteration_value_type) {
4430
+ specifiers.push(b.import_specifier('IterationValue', ITERATION_VALUE_INTERNAL_NAME, 'type'));
4431
+ }
4432
+ imports.push(b.import_declaration(specifiers, platform.imports.forOfIterableHelper));
5302
4433
  }
5303
4434
 
5304
4435
  if (transform_context.needs_error_boundary) {
5305
- const error_boundary_source = platform.imports.errorBoundary;
5306
- imports.push({
5307
- type: 'ImportDeclaration',
5308
- specifiers: [
5309
- {
5310
- type: 'ImportSpecifier',
5311
- imported: {
5312
- type: 'Identifier',
5313
- name: 'TsrxErrorBoundary',
5314
- metadata: { path: [] },
5315
- },
5316
- local: {
5317
- type: 'Identifier',
5318
- name: 'TsrxErrorBoundary',
5319
- metadata: { path: [] },
5320
- },
5321
- metadata: { path: [] },
5322
- },
5323
- ],
5324
- source: {
5325
- type: 'Literal',
5326
- value: error_boundary_source,
5327
- raw: `'${error_boundary_source}'`,
5328
- },
5329
- metadata: { path: [] },
5330
- });
4436
+ imports.push(
4437
+ b.imports([['TsrxErrorBoundary', 'TsrxErrorBoundary']], platform.imports.errorBoundary),
4438
+ );
5331
4439
  }
5332
4440
 
5333
4441
  const merge_refs_source =
@@ -5345,67 +4453,31 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
5345
4453
  const ref_imports = new Map();
5346
4454
 
5347
4455
  if (merge_refs_source !== null) {
5348
- add_ref_import_specifier(ref_imports, merge_refs_source, {
5349
- type: 'ImportSpecifier',
5350
- imported: {
5351
- type: 'Identifier',
5352
- name: 'mergeRefs',
5353
- metadata: { path: [] },
5354
- },
5355
- local: {
5356
- type: 'Identifier',
5357
- name: MERGE_REFS_INTERNAL_NAME,
5358
- metadata: { path: [] },
5359
- },
5360
- metadata: { path: [] },
5361
- });
4456
+ add_ref_import_specifier(
4457
+ ref_imports,
4458
+ merge_refs_source,
4459
+ b.import_specifier('mergeRefs', MERGE_REFS_INTERNAL_NAME),
4460
+ );
5362
4461
  }
5363
4462
 
5364
4463
  if (ref_prop_source !== null) {
5365
- add_ref_import_specifier(ref_imports, ref_prop_source, {
5366
- type: 'ImportSpecifier',
5367
- imported: {
5368
- type: 'Identifier',
5369
- name: 'create_ref_prop',
5370
- metadata: { path: [] },
5371
- },
5372
- local: {
5373
- type: 'Identifier',
5374
- name: CREATE_REF_PROP_INTERNAL_NAME,
5375
- metadata: { path: [] },
5376
- },
5377
- metadata: { path: [] },
5378
- });
4464
+ add_ref_import_specifier(
4465
+ ref_imports,
4466
+ ref_prop_source,
4467
+ b.import_specifier('create_ref_prop', CREATE_REF_PROP_INTERNAL_NAME),
4468
+ );
5379
4469
  }
5380
4470
 
5381
4471
  if (normalize_spread_props_source !== null) {
5382
- add_ref_import_specifier(ref_imports, normalize_spread_props_source, {
5383
- type: 'ImportSpecifier',
5384
- imported: {
5385
- type: 'Identifier',
5386
- name: 'normalize_spread_props',
5387
- metadata: { path: [] },
5388
- },
5389
- local: {
5390
- type: 'Identifier',
5391
- name: NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
5392
- metadata: { path: [] },
5393
- },
5394
- metadata: { path: [] },
5395
- });
4472
+ add_ref_import_specifier(
4473
+ ref_imports,
4474
+ normalize_spread_props_source,
4475
+ b.import_specifier('normalize_spread_props', NORMALIZE_SPREAD_PROPS_INTERNAL_NAME),
4476
+ );
5396
4477
  }
5397
4478
 
5398
4479
  for (const [source, ref_specifiers] of ref_imports) {
5399
- imports.push({
5400
- type: 'ImportDeclaration',
5401
- specifiers: ref_specifiers,
5402
- source: {
5403
- type: 'Literal',
5404
- value: source,
5405
- raw: `'${source}'`,
5406
- },
5407
- metadata: { path: [] },
5408
- });
4480
+ imports.push(b.import_declaration(ref_specifiers, source));
5409
4481
  }
5410
4482
 
5411
4483
  if (imports.length > 0) {
@@ -5453,130 +4525,343 @@ function create_render_if_statement(node, transform_context) {
5453
4525
  true,
5454
4526
  );
5455
4527
  alternate = set_loc(
5456
- /** @type {any} */ ({
5457
- type: 'BlockStatement',
5458
- body: alternate_has_hooks
4528
+ b.block(
4529
+ alternate_has_hooks
5459
4530
  ? hook_safe_render_statements(alternate_body, undefined, transform_context)
5460
4531
  : build_render_statements(alternate_body, true, transform_context),
5461
- metadata: { path: [] },
5462
- }),
4532
+ ),
5463
4533
  node.alternate,
5464
4534
  );
5465
4535
  }
5466
4536
  }
5467
4537
 
5468
4538
  return set_loc(
5469
- {
5470
- type: 'IfStatement',
5471
- test: node.test,
5472
- consequent: set_loc(
5473
- /** @type {any} */ ({
5474
- type: 'BlockStatement',
5475
- body: consequent_has_hooks
4539
+ b.if(
4540
+ node.test,
4541
+ set_loc(
4542
+ b.block(
4543
+ consequent_has_hooks
5476
4544
  ? hook_safe_render_statements(consequent_body, undefined, transform_context)
5477
4545
  : build_render_statements(consequent_body, true, transform_context),
5478
- metadata: { path: [] },
5479
- }),
4546
+ ),
5480
4547
  node.consequent,
5481
4548
  ),
5482
4549
  alternate,
5483
- },
4550
+ ),
5484
4551
  node,
5485
4552
  );
5486
4553
  }
5487
4554
 
5488
4555
  /**
5489
- * @param {any} node
5490
- * @param {TransformContext} transform_context
5491
- * @returns {any}
4556
+ * Per-source-case information used by the switch lift to decide whether each
4557
+ * case body needs to be hoisted into its own helper component or can stay
4558
+ * inline.
4559
+ *
4560
+ * `own_body` is everything in the case's `consequent` up to (and including for
4561
+ * `return <expr>`, excluding for `break` / bare `return;`) the first
4562
+ * terminator. `has_terminator` records whether such a terminator was seen.
4563
+ *
4564
+ * @param {any[]} consequent
4565
+ * @returns {{ own_body: any[], has_terminator: boolean }}
5492
4566
  */
5493
- function create_render_switch_statement(node, transform_context) {
5494
- return /** @type {any} */ ({
5495
- type: 'SwitchStatement',
5496
- discriminant: node.discriminant,
5497
- cases: node.cases.map((/** @type {any} */ c) =>
5498
- create_render_switch_case(c, transform_context),
5499
- ),
5500
- metadata: { path: [] },
5501
- });
4567
+ function summarize_switch_case_body(consequent) {
4568
+ const own_body = [];
4569
+ let has_terminator = false;
4570
+ for (const child of consequent) {
4571
+ if (child.type === 'BreakStatement') {
4572
+ has_terminator = true;
4573
+ break;
4574
+ }
4575
+ if (child.type === 'ReturnStatement' && child.argument == null) {
4576
+ has_terminator = true;
4577
+ break;
4578
+ }
4579
+ own_body.push(child);
4580
+ if (child.type === 'ReturnStatement') {
4581
+ // `return <expr>;` — keep it in own_body so build_render_statements
4582
+ // can emit it as the terminal return for this case, then stop
4583
+ // collecting further nodes.
4584
+ has_terminator = true;
4585
+ break;
4586
+ }
4587
+ }
4588
+ return { own_body, has_terminator };
5502
4589
  }
5503
4590
 
5504
4591
  /**
5505
- * @param {any} switch_case
5506
- * @param {TransformContext} transform_context
4592
+ * Clone a helper's `component_element` for embedding in another case arm or
4593
+ * inside another helper's body. Locations are stripped because the same
4594
+ * element appears in multiple positions; only the helper's *definition* (the
4595
+ * lifted function) keeps the source position so editor IntelliSense doesn't
4596
+ * see double/triple hits per source range.
4597
+ *
4598
+ * @param {{ component_element: ESTreeJSX.JSXElement }} helper
5507
4599
  * @returns {any}
5508
4600
  */
5509
- function create_render_switch_case(switch_case, transform_context) {
5510
- const consequent = flatten_switch_consequent(switch_case.consequent || []);
4601
+ export function clone_switch_helper_invocation(helper) {
4602
+ return clone_expression_node(helper.component_element, false);
4603
+ }
5511
4604
 
5512
- // Strip trailing break statements for hook analysis
5513
- const body_without_break = [];
5514
- for (const child of consequent) {
5515
- if (child.type === 'BreakStatement') break;
5516
- body_without_break.push(child);
5517
- }
4605
+ /**
4606
+ * Plan the switch lift: decide which case bodies to hoist into their own
4607
+ * helper components, build them in reverse so each helper can chain into the
4608
+ * next, and return everything callers need to construct a target-specific
4609
+ * switch shape (a JS `switch` for React/Preact/Vue or `<Switch>/<Match>` for
4610
+ * Solid). Centralizes the lift bookkeeping so both consumers see the same
4611
+ * hook-detection rules, duplication analysis, and helper-id numbering.
4612
+ *
4613
+ * Returned helpers — when non-null — are already constructed via
4614
+ * `create_hook_safe_helper`, which is the same path hook-bearing case bodies
4615
+ * have always used. Locally-scoped helpers have their declarations in
4616
+ * `setup_statements`; module-scoped helpers (the client transform default on
4617
+ * React, Vue, and Solid) already pushed their declarations into
4618
+ * `transform_context.helper_state.helpers`, so `setup_statements` is empty.
4619
+ *
4620
+ * @param {any} switch_node
4621
+ * @param {TransformContext} transform_context
4622
+ * @returns {{
4623
+ * case_info: Array<{ own_body: any[], has_terminator: boolean }>,
4624
+ * case_helpers: Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>,
4625
+ * find_next_helper_after: (from_index: number) => { component_element: ESTreeJSX.JSXElement } | null,
4626
+ * setup_statements: any[],
4627
+ * }}
4628
+ */
4629
+ export function plan_switch_lift(switch_node, transform_context) {
4630
+ const case_info = switch_node.cases.map((/** @type {any} */ c) => {
4631
+ const consequent = flatten_switch_consequent(c.consequent || []);
4632
+ return summarize_switch_case_body(consequent);
4633
+ });
5518
4634
 
5519
- if (body_contains_top_level_hook_call(body_without_break, transform_context, true)) {
5520
- return /** @type {any} */ ({
5521
- type: 'SwitchCase',
5522
- test: switch_case.test,
5523
- consequent: hook_safe_render_statements(body_without_break, undefined, transform_context),
5524
- metadata: { path: [] },
5525
- });
5526
- }
4635
+ // A case body needs to be lifted iff (a) it would render in more than one
4636
+ // arm after fall-through expansion, or (b) it contains hooks (which always
4637
+ // went through the lift pipeline before this change). Duplication happens
4638
+ // exactly when the previous case has no terminator — that's the only way
4639
+ // an earlier arm can reach this body via JS fall-through semantics.
4640
+ const needs_helper = case_info.map(
4641
+ (/** @type {{ own_body: any[], has_terminator: boolean }} */ info, /** @type {number} */ k) => {
4642
+ if (info.own_body.length === 0) return false;
4643
+ if (body_contains_top_level_hook_call(info.own_body, transform_context, true)) {
4644
+ return true;
4645
+ }
4646
+ if (k === 0) return false;
4647
+ return !case_info[k - 1].has_terminator;
4648
+ },
4649
+ );
5527
4650
 
5528
- const case_body = [];
5529
- const render_nodes = [];
5530
- let has_terminal = false;
4651
+ // Pre-allocate helper ids in source order so the snapshot's
4652
+ // `StatementBodyHook<N>` numbering reads top-to-bottom by case position
4653
+ // even though we build helpers in reverse below.
4654
+ /** @type {Array<AST.Identifier | null>} */
4655
+ const helper_ids = needs_helper.map((/** @type {boolean} */ needs) =>
4656
+ needs
4657
+ ? create_generated_identifier(create_local_statement_component_name(transform_context))
4658
+ : null,
4659
+ );
5531
4660
 
5532
- for (const child of consequent) {
5533
- if (child.type === 'BreakStatement') {
5534
- if (render_nodes.length > 0 && !has_terminal) {
5535
- case_body.push(create_component_return_statement(render_nodes, switch_case));
5536
- } else if (!has_terminal) {
5537
- case_body.push(child);
5538
- }
5539
- has_terminal = true;
5540
- break;
5541
- }
4661
+ /** @type {Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>} */
4662
+ const case_helpers = new Array(switch_node.cases.length).fill(null);
5542
4663
 
5543
- if (is_bare_return_statement(child)) {
5544
- case_body.push(create_component_return_statement(render_nodes, child));
5545
- has_terminal = true;
5546
- break;
4664
+ /**
4665
+ * Find the next downstream helper this arm chains into when it has no
4666
+ * terminator: scan forward past any empty cases until we hit either a
4667
+ * helper-bearing case or a case whose body has a terminator (which stops
4668
+ * the chain — JS would have `break`/`return`ed out at that point).
4669
+ *
4670
+ * @param {number} from_index
4671
+ * @returns {{ component_element: ESTreeJSX.JSXElement } | null}
4672
+ */
4673
+ function find_next_helper_after(from_index) {
4674
+ for (let j = from_index + 1; j < switch_node.cases.length; j++) {
4675
+ if (case_helpers[j]) return case_helpers[j];
4676
+ if (case_info[j].has_terminator) return null;
5547
4677
  }
4678
+ return null;
4679
+ }
5548
4680
 
5549
- if (is_jsx_child(child)) {
5550
- render_nodes.push(to_jsx_child(child, transform_context));
5551
- } else if (is_bare_render_expression(child)) {
5552
- render_nodes.push(to_jsx_expression_container(child, child));
5553
- } else {
5554
- case_body.push(child);
4681
+ for (let i = switch_node.cases.length - 1; i >= 0; i--) {
4682
+ if (!needs_helper[i]) continue;
4683
+ const { own_body, has_terminator } = case_info[i];
4684
+
4685
+ let helper_body = own_body;
4686
+ if (!has_terminator) {
4687
+ const next_helper = find_next_helper_after(i);
4688
+ if (next_helper) {
4689
+ helper_body = [...own_body, clone_switch_helper_invocation(next_helper)];
4690
+ }
5555
4691
  }
4692
+
4693
+ case_helpers[i] = create_hook_safe_helper(
4694
+ helper_body,
4695
+ undefined,
4696
+ switch_node.cases[i],
4697
+ transform_context,
4698
+ /** @type {any} */ (helper_ids[i]),
4699
+ );
5556
4700
  }
5557
4701
 
5558
- if (!has_terminal && render_nodes.length > 0) {
5559
- case_body.push(create_component_return_statement(render_nodes, switch_case));
4702
+ // Hoist all helpers' setup statements above the switch in source order so
4703
+ // the switch body stays a pure dispatcher.
4704
+ const setup_statements = [];
4705
+ for (const helper of case_helpers) {
4706
+ if (helper) setup_statements.push(...helper.setup_statements);
5560
4707
  }
5561
4708
 
5562
- return /** @type {any} */ ({
5563
- type: 'SwitchCase',
5564
- test: switch_case.test,
5565
- consequent: case_body,
5566
- metadata: { path: [] },
5567
- });
4709
+ return {
4710
+ case_info,
4711
+ case_helpers,
4712
+ find_next_helper_after,
4713
+ setup_statements,
4714
+ };
5568
4715
  }
5569
4716
 
5570
4717
  /**
5571
- * @returns {any}
4718
+ * Switch lift for fall-through deduplication. Reuses the same `create_hook_safe_helper`
4719
+ * pipeline as hook-bearing case bodies: every case whose body would otherwise
4720
+ * appear in 2+ arms (because the previous case had no `break` / `return`) is
4721
+ * hoisted into its own helper component, and each upstream arm references the
4722
+ * next helper at the end of its own body to materialize JS fall-through at
4723
+ * render time. Cases whose bodies live in exactly one arm stay inline so the
4724
+ * common (break-terminated) shape compiles to the same simple switch as before
4725
+ * the lift was introduced.
4726
+ *
4727
+ * The chain pattern:
4728
+ * helper_idle = () => <><Online/><Helper_active/></>
4729
+ * helper_active = () => <><Away/><Helper_offline/></>
4730
+ * helper_offline = () => <Offline/>
4731
+ *
4732
+ * case "idle": return <Helper_idle/>
4733
+ * case "active": return <Helper_active/>
4734
+ * case "offline": return <Helper_offline/>
4735
+ *
4736
+ * Each case body appears exactly once in the generated module — matching how
4737
+ * we already handle hook-bearing case bodies — which keeps the bundle from
4738
+ * growing quadratically in case count and means editor mappings are 1:1.
4739
+ *
4740
+ * @param {any} switch_node
4741
+ * @param {TransformContext} transform_context
4742
+ * @returns {{ setup_statements: any[], switch_statement: any }}
5572
4743
  */
5573
- function create_null_return_statement() {
4744
+ function build_switch_with_lift(switch_node, transform_context) {
4745
+ const { case_info, case_helpers, find_next_helper_after, setup_statements } = plan_switch_lift(
4746
+ switch_node,
4747
+ transform_context,
4748
+ );
4749
+
4750
+ const new_cases = switch_node.cases.map(
4751
+ (/** @type {any} */ original_case, /** @type {number} */ i) => {
4752
+ const helper = case_helpers[i];
4753
+ if (helper) {
4754
+ return /** @type {any} */ ({
4755
+ type: 'SwitchCase',
4756
+ test: original_case.test,
4757
+ consequent: [
4758
+ create_component_return_statement([helper.component_element], original_case),
4759
+ ],
4760
+ metadata: { path: [] },
4761
+ });
4762
+ }
4763
+
4764
+ const { own_body, has_terminator } = case_info[i];
4765
+
4766
+ if (own_body.length === 0 && !has_terminator) {
4767
+ // Alias-pattern empty case (`case 'a': case 'b': ...`) — keep
4768
+ // the arm body empty so JS falls through to the next case at
4769
+ // runtime, where the helper invocation actually lives.
4770
+ return /** @type {any} */ ({
4771
+ type: 'SwitchCase',
4772
+ test: original_case.test,
4773
+ consequent: [],
4774
+ metadata: { path: [] },
4775
+ });
4776
+ }
4777
+
4778
+ const case_body = [];
4779
+ const render_nodes = [];
4780
+ let has_terminal = false;
4781
+
4782
+ for (const child of own_body) {
4783
+ if (is_bare_return_statement(child)) {
4784
+ case_body.push(create_component_return_statement(render_nodes, child));
4785
+ has_terminal = true;
4786
+ break;
4787
+ }
4788
+ if (child.type === 'ReturnStatement') {
4789
+ case_body.push(child);
4790
+ has_terminal = true;
4791
+ break;
4792
+ }
4793
+ if (is_jsx_child(child)) {
4794
+ render_nodes.push(to_jsx_child(child, transform_context));
4795
+ } else if (is_bare_render_expression(child)) {
4796
+ render_nodes.push(to_jsx_expression_container(child, child));
4797
+ } else {
4798
+ case_body.push(child);
4799
+ }
4800
+ }
4801
+
4802
+ if (!has_terminal && !has_terminator) {
4803
+ const next_helper = find_next_helper_after(i);
4804
+ if (next_helper) {
4805
+ render_nodes.push(clone_switch_helper_invocation(next_helper));
4806
+ }
4807
+ }
4808
+
4809
+ if (!has_terminal) {
4810
+ if (render_nodes.length > 0) {
4811
+ case_body.push(create_component_return_statement(render_nodes, original_case));
4812
+ } else if (has_terminator) {
4813
+ // Empty body with explicit `break;` / bare `return;` — keep
4814
+ // a `break` so JS doesn't fall through into the next case
4815
+ // (which may now hold the lifted helper invocation).
4816
+ case_body.push(
4817
+ /** @type {any} */ ({
4818
+ type: 'BreakStatement',
4819
+ label: null,
4820
+ metadata: { path: [] },
4821
+ }),
4822
+ );
4823
+ } else if (case_body.length > 0) {
4824
+ // Statements-only inline case without terminator. We've
4825
+ // already inlined the downstream chain via the helper
4826
+ // reference above, so emit a `break` to stop the runtime
4827
+ // from re-running downstream statements via JS fall-through.
4828
+ case_body.push(
4829
+ /** @type {any} */ ({
4830
+ type: 'BreakStatement',
4831
+ label: null,
4832
+ metadata: { path: [] },
4833
+ }),
4834
+ );
4835
+ }
4836
+ }
4837
+
4838
+ return /** @type {any} */ ({
4839
+ type: 'SwitchCase',
4840
+ test: original_case.test,
4841
+ consequent: case_body,
4842
+ metadata: { path: [] },
4843
+ });
4844
+ },
4845
+ );
4846
+
5574
4847
  return {
5575
- type: 'ReturnStatement',
5576
- argument: { type: 'Literal', value: null, raw: 'null' },
4848
+ setup_statements,
4849
+ switch_statement: /** @type {any} */ ({
4850
+ type: 'SwitchStatement',
4851
+ discriminant: switch_node.discriminant,
4852
+ cases: new_cases,
4853
+ metadata: { path: [] },
4854
+ }),
5577
4855
  };
5578
4856
  }
5579
4857
 
4858
+ /**
4859
+ * @returns {any}
4860
+ */
4861
+ function create_null_return_statement() {
4862
+ return b.return(b.literal(null));
4863
+ }
4864
+
5580
4865
  /**
5581
4866
  * @param {AST.Expression} expression
5582
4867
  * @param {any} [source_node]
@@ -5675,10 +4960,7 @@ function normalize_named_ref_attributes(attrs, is_host, transform_context) {
5675
4960
  return {
5676
4961
  ...attr,
5677
4962
  metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
5678
- name:
5679
- attr.name?.type === 'JSXIdentifier'
5680
- ? { ...attr.name, name: 'ref' }
5681
- : { type: 'Identifier', name: 'ref', metadata: { path: [] } },
4963
+ name: attr.name?.type === 'JSXIdentifier' ? { ...attr.name, name: 'ref' } : b.id('ref'),
5682
4964
  };
5683
4965
  });
5684
4966
  }
@@ -5819,6 +5101,7 @@ function wrap_jsx_setup_declarations(expression, in_jsx_child) {
5819
5101
  [],
5820
5102
  b.block([...declarations, b.return(return_expression)], expression),
5821
5103
  false,
5104
+ undefined,
5822
5105
  expression,
5823
5106
  ),
5824
5107
  );
@@ -5970,22 +5253,8 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
5970
5253
 
5971
5254
  const merged_value =
5972
5255
  strategy === 'merge-refs'
5973
- ? /** @type {any} */ ({
5974
- type: 'CallExpression',
5975
- callee: {
5976
- type: 'Identifier',
5977
- name: MERGE_REFS_INTERNAL_NAME,
5978
- metadata: { path: [] },
5979
- },
5980
- arguments: ref_exprs,
5981
- optional: false,
5982
- metadata: { path: [] },
5983
- })
5984
- : /** @type {any} */ ({
5985
- type: 'ArrayExpression',
5986
- elements: ref_exprs,
5987
- metadata: { path: [] },
5988
- });
5256
+ ? b.call(b.id(MERGE_REFS_INTERNAL_NAME), ...ref_exprs)
5257
+ : b.array(ref_exprs);
5989
5258
 
5990
5259
  if (strategy === 'merge-refs') {
5991
5260
  transform_context.needs_merge_refs = true;
@@ -5999,11 +5268,7 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
5999
5268
  const merged_name = build_jsx_id('ref', source_attr?.name);
6000
5269
  const merged_attr = build_jsx_attribute(
6001
5270
  merged_name,
6002
- /** @type {any} */ ({
6003
- type: 'JSXExpressionContainer',
6004
- expression: merged_value,
6005
- metadata: { path: [] },
6006
- }),
5271
+ b.jsx_expression_container(merged_value),
6007
5272
  false,
6008
5273
  source_attr,
6009
5274
  );
@@ -6038,6 +5303,8 @@ function is_jsx_ref_attribute(attr) {
6038
5303
  export const MERGE_REFS_INTERNAL_NAME = '__mergeRefs';
6039
5304
  export const CREATE_REF_PROP_INTERNAL_NAME = '__create_ref_prop';
6040
5305
  export const NORMALIZE_SPREAD_PROPS_INTERNAL_NAME = '__normalize_spread_props';
5306
+ export const MAP_ITERABLE_INTERNAL_NAME = '__map_iterable';
5307
+ export const ITERATION_VALUE_INTERNAL_NAME = '__IterationValue';
6041
5308
 
6042
5309
  /**
6043
5310
  * @param {any} attr
@@ -6111,10 +5378,7 @@ export function to_jsx_attribute(attr, transform_context) {
6111
5378
  attr_name.type === 'Identifier' &&
6112
5379
  attr_name.name === 'class'
6113
5380
  ) {
6114
- attr_name = set_loc(
6115
- /** @type {any} */ ({ type: 'Identifier', name: 'className', metadata: { path: [] } }),
6116
- attr.name,
6117
- );
5381
+ attr_name = set_loc(b.id('className'), attr.name);
6118
5382
  }
6119
5383
 
6120
5384
  const name =
@@ -6177,16 +5441,7 @@ function create_ref_prop_call(node, transform_context) {
6177
5441
 
6178
5442
  if (argument.type === 'Identifier' || argument.type === 'MemberExpression') {
6179
5443
  args.push(
6180
- b.arrow(
6181
- [b.id('v')],
6182
- /** @type {any} */ ({
6183
- type: 'AssignmentExpression',
6184
- operator: '=',
6185
- left: clone_expression_node(argument, false),
6186
- right: b.id('v'),
6187
- metadata: { path: [] },
6188
- }),
6189
- ),
5444
+ b.arrow([b.id('v')], b.assignment('=', clone_expression_node(argument, false), b.id('v'))),
6190
5445
  );
6191
5446
  }
6192
5447
 
@@ -6200,57 +5455,19 @@ function create_ref_prop_call(node, transform_context) {
6200
5455
  */
6201
5456
  function dynamic_element_to_jsx_child(node, transform_context) {
6202
5457
  const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
6203
- const alias_declaration = set_loc(
6204
- /** @type {any} */ ({
6205
- type: 'VariableDeclaration',
6206
- kind: 'const',
6207
- declarations: [
6208
- {
6209
- type: 'VariableDeclarator',
6210
- id: dynamic_id,
6211
- init: clone_expression_node(node.id),
6212
- metadata: { path: [] },
6213
- },
6214
- ],
6215
- metadata: { path: [] },
6216
- }),
6217
- node,
6218
- );
5458
+ const alias_declaration = set_loc(b.const(dynamic_id, clone_expression_node(node.id)), node);
6219
5459
  const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
6220
5460
 
6221
5461
  return to_jsx_expression_container(
6222
- /** @type {any} */ ({
6223
- type: 'CallExpression',
6224
- callee: {
6225
- type: 'ArrowFunctionExpression',
6226
- params: [],
6227
- body: /** @type {any} */ ({
6228
- type: 'BlockStatement',
6229
- body: [
6230
- alias_declaration,
6231
- {
6232
- type: 'ReturnStatement',
6233
- argument: {
6234
- type: 'ConditionalExpression',
6235
- test: clone_identifier(dynamic_id),
6236
- consequent: jsx_element,
6237
- alternate: create_null_literal(),
6238
- metadata: { path: [] },
6239
- },
6240
- metadata: { path: [] },
6241
- },
6242
- ],
6243
- metadata: { path: [] },
6244
- }),
6245
- async: false,
6246
- generator: false,
6247
- expression: false,
6248
- metadata: { path: [] },
6249
- },
6250
- arguments: [],
6251
- optional: false,
6252
- metadata: { path: [] },
6253
- }),
5462
+ b.call(
5463
+ b.arrow(
5464
+ [],
5465
+ b.block([
5466
+ alias_declaration,
5467
+ b.return(b.conditional(clone_identifier(dynamic_id), jsx_element, create_null_literal())),
5468
+ ]),
5469
+ ),
5470
+ ),
6254
5471
  node,
6255
5472
  );
6256
5473
  }