@tsrx/core 0.0.20 → 0.0.22

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.20",
6
+ "version": "0.0.22",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -250,17 +250,46 @@ export function flatten_switch_consequent(consequent) {
250
250
  return result;
251
251
  }
252
252
 
253
+ /**
254
+ * @param {AST.Expression | null | undefined} expression
255
+ * @returns {boolean}
256
+ */
257
+ function is_static_string_expression(expression) {
258
+ if (!expression) {
259
+ return false;
260
+ }
261
+ if (expression.type === 'Literal') {
262
+ return typeof expression.value === 'string';
263
+ }
264
+ if (expression.type === 'TemplateLiteral') {
265
+ return expression.expressions.length === 0;
266
+ }
267
+ return false;
268
+ }
269
+
253
270
  /**
254
271
  * Build `expr == null ? '' : expr + ''` — the text-coerce form used when a
255
272
  * Ripple `{expr}` child must render as a string in JSX (React/Preact drop
256
273
  * booleans; Solid's default child semantics don't either). Solid uses this
257
274
  * via `to_jsx_child`; React/Preact wrap it in a JSXExpressionContainer.
258
275
  *
276
+ * When the expression is statically a non-null string at the AST level —
277
+ * a string `Literal` (`"hello"`, `'hello'`) or a `TemplateLiteral` with no
278
+ * interpolations (`` `hello` ``) — the coercion is provably a no-op and
279
+ * the literal is emitted as-is. This covers both direct double-quoted
280
+ * children (`<b>"hello"</b>`) and inline literal arguments to the explicit
281
+ * `{text ...}` intrinsic (`<b>{text 'hello'}</b>`). Identifiers and any
282
+ * other expression type still get the ternary because the AST alone can't
283
+ * prove they're non-null strings.
284
+ *
259
285
  * @param {AST.Expression} expression
260
286
  * @param {any} [source_node]
261
287
  * @returns {AST.Expression}
262
288
  */
263
289
  export function to_text_expression(expression, source_node = expression) {
290
+ if (is_static_string_expression(expression)) {
291
+ return set_loc(clone_expression_node(expression), source_node);
292
+ }
264
293
  return set_loc(
265
294
  /** @type {AST.Expression} */ ({
266
295
  type: 'ConditionalExpression',
@@ -186,7 +186,31 @@ export function tsx_with_ts_locations() {
186
186
  context.visit(body);
187
187
  }
188
188
  },
189
+
190
+ // esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
191
+ // component tags like `<RenderProp<User>>` lose the `<User>` in the output.
192
+ JSXOpeningElement: (node, context) => {
193
+ context.write('<');
194
+ context.visit(node.name);
195
+ if (node.typeArguments) {
196
+ context.visit(node.typeArguments);
197
+ }
198
+ for (const attribute of node.attributes) {
199
+ context.write(' ');
200
+ context.visit(attribute);
201
+ }
202
+ if (node.selfClosing) {
203
+ context.write(' /');
204
+ }
205
+ context.write('>');
206
+ },
189
207
  };
208
+
209
+ // Be careful when duplicating visitors that are already defined
210
+ // above in the `wrappers`
211
+ // if there is already a visitor but you still need a mapping
212
+ // on the whole node, only then duplicate it here
213
+ // e.g. JSXOpeningElement is such a case
190
214
  for (const type of [
191
215
  // JS nodes whose esrap printer emits no location marker, causing
192
216
  // segments.js get_mapping_from_node() to throw when it asks for the
@@ -214,7 +238,9 @@ export function tsx_with_ts_locations() {
214
238
  'TSTypeParameterDeclaration',
215
239
  'TSTypeParameter',
216
240
  ]) {
217
- wrappers[type] = (node, context) => wrap_with_locations(node, context, base[type]);
241
+ const visitor = wrappers[type];
242
+
243
+ wrappers[type] = (node, context) => wrap_with_locations(node, context, visitor ?? base[type]);
218
244
  }
219
245
 
220
246
  return { ...base, ...wrappers };
@@ -31,6 +31,7 @@ import {
31
31
  jsx_attribute as build_jsx_attribute,
32
32
  jsx_id as build_jsx_id,
33
33
  } from '../../utils/builders.js';
34
+ import * as b from '../../utils/builders.js';
34
35
  import {
35
36
  apply_lazy_transforms,
36
37
  collect_lazy_bindings_from_component,
@@ -589,6 +590,105 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
589
590
  continue;
590
591
  }
591
592
 
593
+ if (
594
+ child.type === 'IfStatement' &&
595
+ !child.alternate &&
596
+ !is_returning_if_statement(child) &&
597
+ !transform_context.platform.hooks?.isTopLevelSetupCall &&
598
+ body_contains_top_level_hook_call([child], transform_context, true) &&
599
+ i + 1 < body_nodes.length
600
+ ) {
601
+ statements.push(
602
+ ...create_continuation_lift_if_statement(
603
+ child,
604
+ body_nodes.slice(i + 1),
605
+ render_nodes,
606
+ transform_context,
607
+ ),
608
+ );
609
+ transform_context.available_bindings = saved_bindings;
610
+ return statements;
611
+ }
612
+
613
+ if (
614
+ child.type === 'ForOfStatement' &&
615
+ !child.await &&
616
+ !transform_context.platform.hooks?.isTopLevelSetupCall &&
617
+ !transform_context.platform.hooks?.controlFlow?.forOf &&
618
+ body_contains_top_level_hook_call(
619
+ child.body.type === 'BlockStatement' ? child.body.body : [child.body],
620
+ transform_context,
621
+ true,
622
+ )
623
+ ) {
624
+ const for_of_continuation = body_nodes.slice(i + 1);
625
+ const hoisted = build_hoisted_for_of_with_hooks(
626
+ child,
627
+ for_of_continuation,
628
+ transform_context,
629
+ );
630
+ if (hoisted) {
631
+ statements.push(...hoisted.hoist_statements);
632
+ if (for_of_continuation.length > 0) {
633
+ // Tail was lifted into the helper; everything after the for-of
634
+ // now lives there. Combine prior render_nodes with the iteration
635
+ // JSX and return.
636
+ statements.push({
637
+ type: 'ReturnStatement',
638
+ argument: combine_render_return_argument(render_nodes, hoisted.jsx_child),
639
+ metadata: { path: [] },
640
+ });
641
+ transform_context.available_bindings = saved_bindings;
642
+ return statements;
643
+ }
644
+ if (interleaved && is_capturable_jsx_child(hoisted.jsx_child)) {
645
+ const { declaration, reference } = captureJsxChild(hoisted.jsx_child, capture_index++);
646
+ statements.push(declaration);
647
+ render_nodes.push(reference);
648
+ } else {
649
+ render_nodes.push(hoisted.jsx_child);
650
+ }
651
+ continue;
652
+ }
653
+ }
654
+
655
+ if (
656
+ child.type === 'TryStatement' &&
657
+ !child.finalizer &&
658
+ !transform_context.platform.hooks?.isTopLevelSetupCall &&
659
+ try_statement_contains_hooks(child, transform_context) &&
660
+ i + 1 < body_nodes.length
661
+ ) {
662
+ statements.push(
663
+ ...create_continuation_lift_try_statement(
664
+ child,
665
+ body_nodes.slice(i + 1),
666
+ render_nodes,
667
+ transform_context,
668
+ ),
669
+ );
670
+ transform_context.available_bindings = saved_bindings;
671
+ return statements;
672
+ }
673
+
674
+ if (
675
+ child.type === 'SwitchStatement' &&
676
+ !transform_context.platform.hooks?.isTopLevelSetupCall &&
677
+ body_contains_top_level_hook_call([child], transform_context, true) &&
678
+ i + 1 < body_nodes.length
679
+ ) {
680
+ statements.push(
681
+ ...create_continuation_lift_switch_statement(
682
+ child,
683
+ body_nodes.slice(i + 1),
684
+ render_nodes,
685
+ transform_context,
686
+ ),
687
+ );
688
+ transform_context.available_bindings = saved_bindings;
689
+ return statements;
690
+ }
691
+
592
692
  if (is_jsx_child(child)) {
593
693
  const jsx = to_jsx_child(child, transform_context);
594
694
  if (interleaved && is_capturable_jsx_child(jsx)) {
@@ -838,35 +938,26 @@ function create_helper_props_property(binding) {
838
938
  */
839
939
  function create_helper_component_element(helper_id, bindings, source_node, mapping = {}) {
840
940
  const { mapWrapper = true, mapBindingNames = true, mapBindingValues = true } = mapping;
841
- const attributes = bindings.map(
842
- (binding) =>
843
- /** @type {any} */ ({
844
- type: 'JSXAttribute',
845
- name: identifier_to_jsx_name(
846
- mapBindingNames ? clone_identifier(binding) : create_generated_identifier(binding.name),
847
- ),
848
- value: to_jsx_expression_container(
849
- mapBindingValues ? clone_identifier(binding) : create_generated_identifier(binding.name),
850
- binding,
851
- ),
852
- metadata: { path: [] },
853
- }),
941
+ const attributes = bindings.map((binding) =>
942
+ b.jsx_attribute(
943
+ identifier_to_jsx_name(
944
+ mapBindingNames ? clone_identifier(binding) : create_generated_identifier(binding.name),
945
+ ),
946
+ to_jsx_expression_container(
947
+ mapBindingValues ? clone_identifier(binding) : create_generated_identifier(binding.name),
948
+ binding,
949
+ ),
950
+ ),
854
951
  );
855
952
 
856
- const openingElement = {
857
- type: 'JSXOpeningElement',
858
- name: identifier_to_jsx_name(clone_identifier(helper_id)),
953
+ const opening_element = b.jsx_opening_element(
954
+ identifier_to_jsx_name(clone_identifier(helper_id)),
859
955
  attributes,
860
- selfClosing: true,
861
- metadata: { path: [] },
862
- };
863
- const element = /** @type {any} */ ({
864
- type: 'JSXElement',
865
- openingElement: mapWrapper ? set_loc(openingElement, source_node) : openingElement,
866
- closingElement: null,
867
- children: [],
868
- metadata: { path: [] },
869
- });
956
+ true,
957
+ );
958
+ const element = b.jsx_element_fresh(
959
+ mapWrapper ? set_loc(opening_element, source_node) : opening_element,
960
+ );
870
961
 
871
962
  return mapWrapper ? set_loc(element, source_node) : element;
872
963
  }
@@ -1056,21 +1147,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1056
1147
  const name = create_helper_name(transform_context.helper_state, 'static');
1057
1148
  const id = create_generated_identifier(name);
1058
1149
 
1059
- transform_context.helper_state.statics.push(
1060
- /** @type {any} */ ({
1061
- type: 'VariableDeclaration',
1062
- kind: 'const',
1063
- declarations: [
1064
- {
1065
- type: 'VariableDeclarator',
1066
- id,
1067
- init: node,
1068
- metadata: { path: [] },
1069
- },
1070
- ],
1071
- metadata: { path: [] },
1072
- }),
1073
- );
1150
+ transform_context.helper_state.statics.push(b.const(id, node));
1074
1151
 
1075
1152
  render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
1076
1153
  }
@@ -1176,25 +1253,13 @@ function create_component_return_statement(
1176
1253
  source_node,
1177
1254
  map_render_node_locations = true,
1178
1255
  ) {
1179
- return set_loc(
1180
- /** @type {any} */ ({
1181
- type: 'ReturnStatement',
1182
- argument: build_return_expression(
1183
- render_nodes.map((node) =>
1184
- map_render_node_locations
1185
- ? clone_expression_node(node)
1186
- : clone_expression_node_without_locations(node),
1187
- ),
1188
- ) || {
1189
- type: 'Literal',
1190
- value: null,
1191
- raw: 'null',
1192
- metadata: { path: [] },
1193
- },
1194
- metadata: { path: [] },
1195
- }),
1196
- source_node,
1256
+ const cloned = render_nodes.map((node) =>
1257
+ map_render_node_locations
1258
+ ? clone_expression_node(node)
1259
+ : clone_expression_node_without_locations(node),
1197
1260
  );
1261
+
1262
+ return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
1198
1263
  }
1199
1264
 
1200
1265
  /**
@@ -1206,20 +1271,14 @@ function create_component_lone_return_if_statement(node, render_nodes) {
1206
1271
  const consequent_body = get_if_consequent_body(node);
1207
1272
 
1208
1273
  return set_loc(
1209
- /** @type {any} */ ({
1210
- type: 'IfStatement',
1211
- test: node.test,
1212
- consequent: set_loc(
1213
- /** @type {any} */ ({
1214
- type: 'BlockStatement',
1215
- body: [create_component_return_statement(render_nodes, consequent_body[0], false)],
1216
- metadata: { path: [] },
1217
- }),
1274
+ b.if(
1275
+ node.test,
1276
+ set_loc(
1277
+ b.block([create_component_return_statement(render_nodes, consequent_body[0], false)]),
1218
1278
  node.consequent,
1219
1279
  ),
1220
- alternate: null,
1221
- metadata: { path: [] },
1222
- }),
1280
+ null,
1281
+ ),
1223
1282
  node,
1224
1283
  );
1225
1284
  }
@@ -1235,23 +1294,84 @@ function create_component_returning_if_statement(node, render_nodes, transform_c
1235
1294
  const branch_statements = build_render_statements(consequent_body, true, transform_context);
1236
1295
  prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
1237
1296
 
1238
- return set_loc(
1239
- /** @type {any} */ ({
1240
- type: 'IfStatement',
1241
- test: node.test,
1242
- consequent: set_loc(
1243
- /** @type {any} */ ({
1244
- type: 'BlockStatement',
1245
- body: branch_statements,
1246
- metadata: { path: [] },
1247
- }),
1248
- node.consequent,
1249
- ),
1250
- alternate: null,
1251
- metadata: { path: [] },
1252
- }),
1253
- node,
1254
- );
1297
+ return set_loc(b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null), node);
1298
+ }
1299
+
1300
+ /* ---------------------------------------------------------------------- *
1301
+ * Continuation-lift primitives shared across if / switch / try / for-of *
1302
+ * ---------------------------------------------------------------------- */
1303
+
1304
+ /**
1305
+ * Build the helper component that owns the post-control-flow continuation.
1306
+ * Same shape as `create_hook_safe_helper`; named for intent at lift call sites.
1307
+ *
1308
+ * @param {any[]} continuation_body
1309
+ * @param {any} source_node
1310
+ * @param {TransformContext} transform_context
1311
+ * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
1312
+ */
1313
+ function build_tail_helper(continuation_body, source_node, transform_context) {
1314
+ return create_hook_safe_helper(continuation_body, undefined, source_node, transform_context);
1315
+ }
1316
+
1317
+ /**
1318
+ * Clone the tail helper's component element for embedding inside another
1319
+ * branch's body. Loses location info because the same element appears in
1320
+ * multiple positions and downstream tooling treats AST nodes as identity-keyed.
1321
+ *
1322
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1323
+ * @returns {any}
1324
+ */
1325
+ function clone_tail_invocation(tail_helper) {
1326
+ return clone_expression_node_without_locations(tail_helper.component_element);
1327
+ }
1328
+
1329
+ /**
1330
+ * Return `[...body, <TailHelper x={x} />]` so the branch's render output
1331
+ * includes the tail invocation and the post-hook locals flow forward.
1332
+ * Used by if / switch / try (unconditional append). For-of uses a different
1333
+ * shape — gating on `_tsrx_isLast_<n>` — so it constructs its own.
1334
+ *
1335
+ * @param {any[]} body
1336
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1337
+ * @returns {any[]}
1338
+ */
1339
+ function append_tail_invocation(body, tail_helper) {
1340
+ return [...body, clone_tail_invocation(tail_helper)];
1341
+ }
1342
+
1343
+ /**
1344
+ * Build a `return <combined-render-fragment>;` statement, prepending any
1345
+ * `render_nodes` collected before the control-flow construct so they don't
1346
+ * get dropped on the lift path.
1347
+ *
1348
+ * @param {any[]} render_nodes
1349
+ * @param {any} jsx_child
1350
+ * @returns {any}
1351
+ */
1352
+ function combined_return_statement(render_nodes, jsx_child) {
1353
+ return b.return(combine_render_return_argument(render_nodes, jsx_child));
1354
+ }
1355
+
1356
+ /**
1357
+ * Hoist a for-of iteration source into a generated `let` and add a
1358
+ * normalization assignment via `Array.isArray(src) ? src : Array.from(src)`.
1359
+ * Always emits both — even when the source is already a simple identifier —
1360
+ * so the loop-scoped TS type aliases have a stable name to reference and the
1361
+ * runtime check skips the copy when the value is already an array.
1362
+ *
1363
+ * @param {AST.Identifier} source_id
1364
+ * @param {any} source_expr
1365
+ * @returns {{ source_decl: any, source_normalize_decl: any }}
1366
+ */
1367
+ function build_array_normalization_decls(source_id, source_expr) {
1368
+ const source_decl = b.let(clone_identifier(source_id), clone_expression_node(source_expr));
1369
+ const is_array_call = b.call(b.member(b.id('Array'), 'isArray'), clone_identifier(source_id));
1370
+ const from_call = b.call(b.member(b.id('Array'), 'from'), clone_identifier(source_id));
1371
+ const normalized = b.conditional(is_array_call, clone_identifier(source_id), from_call);
1372
+ const source_normalize_decl = b.stmt(b.assignment('=', clone_identifier(source_id), normalized));
1373
+
1374
+ return { source_decl, source_normalize_decl };
1255
1375
  }
1256
1376
 
1257
1377
  /**
@@ -1283,43 +1403,582 @@ function create_component_helper_split_returning_if_statements(
1283
1403
  node,
1284
1404
  transform_context,
1285
1405
  );
1406
+
1407
+ const branch_block = set_loc(
1408
+ b.block([
1409
+ ...branch_helper.setup_statements,
1410
+ combined_return_statement(render_nodes, branch_helper.component_element),
1411
+ ]),
1412
+ node.consequent,
1413
+ );
1414
+
1286
1415
  return [
1287
- set_loc(
1288
- /** @type {any} */ ({
1289
- type: 'IfStatement',
1290
- test: node.test,
1291
- consequent: set_loc(
1292
- /** @type {any} */ ({
1293
- type: 'BlockStatement',
1294
- body: [
1295
- ...branch_helper.setup_statements,
1296
- {
1297
- type: 'ReturnStatement',
1298
- argument: combine_render_return_argument(
1299
- render_nodes,
1300
- branch_helper.component_element,
1301
- ),
1302
- metadata: { path: [] },
1303
- },
1304
- ],
1305
- metadata: { path: [] },
1306
- }),
1307
- node.consequent,
1308
- ),
1309
- alternate: null,
1310
- metadata: { path: [] },
1311
- }),
1312
- node,
1313
- ),
1416
+ set_loc(b.if(node.test, branch_block, null), node),
1314
1417
  ...continuation_helper.setup_statements,
1315
- {
1316
- type: 'ReturnStatement',
1317
- argument: combine_render_return_argument(render_nodes, continuation_helper.component_element),
1318
- metadata: { path: [] },
1418
+ combined_return_statement(render_nodes, continuation_helper.component_element),
1419
+ ];
1420
+ }
1421
+
1422
+ /**
1423
+ * Lift a non-returning `if` whose consequent contains hook calls plus the
1424
+ * statements that follow it into helper components.
1425
+ *
1426
+ * Without this, the consequent's hook would be wrapped into a child component
1427
+ * (StatementBodyHook) but any code after the `if` that reads bindings the hook
1428
+ * mutates would observe the pre-hook value, because React commits children
1429
+ * after their parent has finished rendering. The fix mirrors the early-return
1430
+ * splitter: emit a tail helper that owns the post-`if` statements, append a
1431
+ * call to it inside the branch helper so the post-hook bindings flow forward,
1432
+ * and render the tail helper directly when the `if` is false.
1433
+ *
1434
+ * @param {any} if_node
1435
+ * @param {any[]} continuation_body
1436
+ * @param {any[]} render_nodes
1437
+ * @param {TransformContext} transform_context
1438
+ * @returns {any[]}
1439
+ */
1440
+ function create_continuation_lift_if_statement(
1441
+ if_node,
1442
+ continuation_body,
1443
+ render_nodes,
1444
+ transform_context,
1445
+ ) {
1446
+ const consequent_body = get_if_consequent_body(if_node);
1447
+ const tail_helper = build_tail_helper(continuation_body, if_node, transform_context);
1448
+ const branch_helper = create_hook_safe_helper(
1449
+ append_tail_invocation(consequent_body, tail_helper),
1450
+ undefined,
1451
+ if_node.consequent,
1452
+ transform_context,
1453
+ );
1454
+
1455
+ const branch_block = set_loc(
1456
+ b.block([
1457
+ ...branch_helper.setup_statements,
1458
+ combined_return_statement(render_nodes, branch_helper.component_element),
1459
+ ]),
1460
+ if_node.consequent,
1461
+ );
1462
+
1463
+ return [
1464
+ ...tail_helper.setup_statements,
1465
+ set_loc(b.if(if_node.test, branch_block, null), if_node),
1466
+ combined_return_statement(render_nodes, tail_helper.component_element),
1467
+ ];
1468
+ }
1469
+
1470
+ /**
1471
+ * Continuation lift for `try` / `try / pending / catch` statements. Same
1472
+ * shape as if/switch: build a tail helper from the post-`try` statements, and
1473
+ * append a clone of its invocation to the try body and the catch body so the
1474
+ * post-hook locals inside each branch flow forward into the tail. The pending
1475
+ * body is left untouched — when Suspense renders the pending fallback the
1476
+ * parent's render is unwound, so the tail wouldn't run in source semantics
1477
+ * either. Once augmented, the existing try transform builds the
1478
+ * Suspense / TsrxErrorBoundary wrapper as usual.
1479
+ *
1480
+ * @param {any} node - TryStatement
1481
+ * @param {any[]} continuation_body
1482
+ * @param {any[]} render_nodes
1483
+ * @param {TransformContext} transform_context
1484
+ * @returns {any[]}
1485
+ */
1486
+ function create_continuation_lift_try_statement(
1487
+ node,
1488
+ continuation_body,
1489
+ render_nodes,
1490
+ transform_context,
1491
+ ) {
1492
+ const tail_helper = build_tail_helper(continuation_body, node, transform_context);
1493
+
1494
+ const augmented_block = {
1495
+ ...node.block,
1496
+ body: append_tail_invocation(node.block.body || [], tail_helper),
1497
+ };
1498
+
1499
+ let augmented_handler = node.handler;
1500
+ if (node.handler) {
1501
+ augmented_handler = {
1502
+ ...node.handler,
1503
+ body: {
1504
+ ...node.handler.body,
1505
+ body: append_tail_invocation(node.handler.body.body || [], tail_helper),
1506
+ },
1507
+ };
1508
+ }
1509
+
1510
+ const augmented_try = {
1511
+ ...node,
1512
+ block: augmented_block,
1513
+ handler: augmented_handler,
1514
+ };
1515
+
1516
+ const try_jsx_child = (
1517
+ transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
1518
+ )(augmented_try, transform_context);
1519
+
1520
+ return [...tail_helper.setup_statements, combined_return_statement(render_nodes, try_jsx_child)];
1521
+ }
1522
+
1523
+ /**
1524
+ * @param {any} node - TryStatement
1525
+ * @param {TransformContext} transform_context
1526
+ * @returns {boolean}
1527
+ */
1528
+ function try_statement_contains_hooks(node, transform_context) {
1529
+ if (body_contains_top_level_hook_call(node.block?.body || [], transform_context, true)) {
1530
+ return true;
1531
+ }
1532
+ if (
1533
+ node.handler &&
1534
+ body_contains_top_level_hook_call(node.handler.body?.body || [], transform_context, true)
1535
+ ) {
1536
+ return true;
1537
+ }
1538
+ if (
1539
+ node.pending &&
1540
+ body_contains_top_level_hook_call(node.pending.body || [], transform_context, true)
1541
+ ) {
1542
+ return true;
1543
+ }
1544
+ return false;
1545
+ }
1546
+
1547
+ /**
1548
+ * Continuation lift for `switch` statements. Same shape as the if-version:
1549
+ * each case body is wrapped in its own helper component that ends with a
1550
+ * call to a shared tail helper, so post-hook bindings inside any case flow
1551
+ * forward to the statements after the switch. The fall-through return at
1552
+ * the end renders the tail helper directly, covering the case where no
1553
+ * `case` (and no `default`) matched.
1554
+ *
1555
+ * Empty fall-through cases (`case 'a':` with no body, falling through to
1556
+ * the next case) are preserved as-is — they must not get their own helper
1557
+ * because that would convert fall-through into early-return.
1558
+ *
1559
+ * @param {any} switch_node
1560
+ * @param {any[]} continuation_body
1561
+ * @param {any[]} render_nodes
1562
+ * @param {TransformContext} transform_context
1563
+ * @returns {any[]}
1564
+ */
1565
+ function create_continuation_lift_switch_statement(
1566
+ switch_node,
1567
+ continuation_body,
1568
+ render_nodes,
1569
+ transform_context,
1570
+ ) {
1571
+ const tail_helper = build_tail_helper(continuation_body, switch_node, transform_context);
1572
+
1573
+ // Per-case info computed once: own body (statements before any
1574
+ // terminator) and whether the case has a `break` / `return`.
1575
+ const case_info = switch_node.cases.map((/** @type {any} */ c) => {
1576
+ const consequent = flatten_switch_consequent(c.consequent || []);
1577
+ const own_body = [];
1578
+ let own_has_terminator = false;
1579
+ for (const node of consequent) {
1580
+ if (node.type === 'BreakStatement' || node.type === 'ReturnStatement') {
1581
+ own_has_terminator = true;
1582
+ break;
1583
+ }
1584
+ own_body.push(node);
1585
+ }
1586
+ return { own_body, own_has_terminator };
1587
+ });
1588
+
1589
+ // Allocate helper ids in source order (forward pass) so the snapshot's
1590
+ // `StatementBodyHook<N>` numbering reads top-to-bottom by case position.
1591
+ /** @type {Array<AST.Identifier | null>} */
1592
+ const helper_ids = case_info.map(
1593
+ (/** @type {{ own_body: any[], own_has_terminator: boolean }} */ info) =>
1594
+ info.own_body.length === 0
1595
+ ? null
1596
+ : create_generated_identifier(create_local_statement_component_name(transform_context)),
1597
+ );
1598
+
1599
+ // Build helpers in reverse order: each fall-through case's helper body
1600
+ // invokes the *next* case's helper, so the chain forwards post-mutation
1601
+ // locals through the switch. Reverse iteration ensures the next helper's
1602
+ // component_element is already constructed when we need to embed it.
1603
+ /** @type {Array<{ setup_statements: any[], component_element: any } | null>} */
1604
+ const case_helper_by_index = new Array(switch_node.cases.length).fill(null);
1605
+ for (let i = switch_node.cases.length - 1; i >= 0; i--) {
1606
+ const { own_body, own_has_terminator } = case_info[i];
1607
+ if (own_body.length === 0) continue;
1608
+
1609
+ // Determine the downstream helper this case invokes after its own body.
1610
+ // - With a terminator: invoke the tail helper directly (case exits switch).
1611
+ // - Otherwise (fall-through): invoke the next non-empty case's helper,
1612
+ // or the tail if nothing else follows.
1613
+ let downstream;
1614
+ if (own_has_terminator) {
1615
+ downstream = tail_helper;
1616
+ } else {
1617
+ let next_helper = null;
1618
+ for (let j = i + 1; j < switch_node.cases.length; j++) {
1619
+ if (case_helper_by_index[j]) {
1620
+ next_helper = case_helper_by_index[j];
1621
+ break;
1622
+ }
1623
+ }
1624
+ downstream = next_helper ?? tail_helper;
1625
+ }
1626
+
1627
+ case_helper_by_index[i] = create_hook_safe_helper(
1628
+ append_tail_invocation(own_body, downstream),
1629
+ undefined,
1630
+ switch_node.cases[i],
1631
+ transform_context,
1632
+ /** @type {any} */ (helper_ids[i]),
1633
+ );
1634
+ }
1635
+
1636
+ const new_cases = switch_node.cases.map(
1637
+ (/** @type {any} */ original_case, /** @type {number} */ i) => {
1638
+ const helper = case_helper_by_index[i];
1639
+ if (helper) {
1640
+ return b.switch_case(original_case.test, [
1641
+ combined_return_statement(render_nodes, helper.component_element),
1642
+ ]);
1643
+ }
1644
+
1645
+ const { own_body, own_has_terminator } = case_info[i];
1646
+ if (own_body.length === 0 && own_has_terminator) {
1647
+ // `case 'a': break;` — exits the switch, then runs the tail.
1648
+ return b.switch_case(original_case.test, [
1649
+ combined_return_statement(render_nodes, tail_helper.component_element),
1650
+ ]);
1651
+ }
1652
+ // Genuine empty fall-through (`case 'a': case 'b': ...`).
1653
+ return b.switch_case(original_case.test, []);
1319
1654
  },
1655
+ );
1656
+
1657
+ // Hoist all case helpers' setup statements above the switch in source
1658
+ // order so the switch body is purely a dispatcher.
1659
+ const case_helper_setup_statements = [];
1660
+ for (const helper of case_helper_by_index) {
1661
+ if (helper) case_helper_setup_statements.push(...helper.setup_statements);
1662
+ }
1663
+
1664
+ return [
1665
+ ...tail_helper.setup_statements,
1666
+ ...case_helper_setup_statements,
1667
+ set_loc(b.switch(switch_node.discriminant, new_cases), switch_node),
1668
+ combined_return_statement(render_nodes, tail_helper.component_element),
1320
1669
  ];
1321
1670
  }
1322
1671
 
1672
+ /**
1673
+ * Hoist the helper for a hook-bearing for-of body out of the iteration
1674
+ * callback so the helper is declared once per render rather than re-bound on
1675
+ * every iteration. Loop-scoped param types are derived from the iteration
1676
+ * source via a TS `type` alias (rather than the const+typeof pattern used
1677
+ * for outer bindings, which would require the loop var to be in scope).
1678
+ *
1679
+ * The iteration source is hoisted into a generated `let` and normalized via
1680
+ * `Array.isArray(src) ? src : Array.from(src)` so any Iterable / ArrayLike
1681
+ * works while skipping the copy when the source is already an array. The
1682
+ * iteration itself is emitted as `source.map((item, i) => ...)`.
1683
+ *
1684
+ * If `continuation_body` is non-empty (the for-of has a tail) we also lift
1685
+ * the tail into a TailHelper and call it conditionally on the last iteration
1686
+ * via an `isLast={i === source.length - 1}` prop on the loop helper. The
1687
+ * loop helper's mutated locals (post-`useState`) flow into the TailHelper as
1688
+ * its props. When the source is empty, `.map` returns `[]` and the TailHelper
1689
+ * never renders — we add a sibling fallback so the source's tail still runs
1690
+ * with the original outer values in that case.
1691
+ *
1692
+ * Bails out (returns null) when the loop pattern is destructured — deriving
1693
+ * element types from a tuple/object pattern is more involved and deferred.
1694
+ *
1695
+ * @param {any} node - ForOfStatement
1696
+ * @param {any[]} continuation_body
1697
+ * @param {TransformContext} transform_context
1698
+ * @returns {{ hoist_statements: any[], jsx_child: any } | null}
1699
+ */
1700
+ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_context) {
1701
+ const loop_params = get_for_of_iteration_params(node.left, node.index);
1702
+ for (const param of loop_params) {
1703
+ if (param.type !== 'Identifier') return null;
1704
+ }
1705
+
1706
+ const has_tail = continuation_body.length > 0;
1707
+ const original_loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
1708
+
1709
+ // When there's a tail, build TailHelper first so its component_element can
1710
+ // be embedded inside the loop helper's body (gated on isLast). The
1711
+ // synthetic isLast prop uses the loop helper's index (which will be the
1712
+ // next one assigned, since `create_hook_safe_helper` for the tail just
1713
+ // consumed one) so it lines up with `StatementBodyHook<N>` in the output.
1714
+ let tail_helper = null;
1715
+ /** @type {AST.Identifier} */ let tail_synthetic_id;
1716
+ if (has_tail) {
1717
+ tail_helper = build_tail_helper(continuation_body, node, transform_context);
1718
+ tail_synthetic_id = create_generated_identifier(
1719
+ `_tsrx_isLast_${transform_context.local_statement_component_index + 1}`,
1720
+ );
1721
+ } else {
1722
+ tail_synthetic_id = /** @type {any} */ (null);
1723
+ }
1724
+ const loop_body = has_tail
1725
+ ? [
1726
+ ...original_loop_body,
1727
+ b.jsx_expression_container(
1728
+ b.logical(
1729
+ '&&',
1730
+ clone_identifier(tail_synthetic_id),
1731
+ clone_tail_invocation(/** @type {any} */ (tail_helper)),
1732
+ ),
1733
+ ),
1734
+ ]
1735
+ : original_loop_body;
1736
+
1737
+ const source_id = create_generated_identifier(
1738
+ `_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
1739
+ );
1740
+ const { source_decl, source_normalize_decl } = build_array_normalization_decls(
1741
+ source_id,
1742
+ node.right,
1743
+ );
1744
+
1745
+ const saved_bindings = transform_context.available_bindings;
1746
+ transform_context.available_bindings = new Map(saved_bindings);
1747
+ for (const param of loop_params) {
1748
+ collect_pattern_bindings(param, transform_context.available_bindings);
1749
+ }
1750
+
1751
+ const all_helper_bindings = get_referenced_helper_bindings(
1752
+ loop_body,
1753
+ transform_context.available_bindings,
1754
+ );
1755
+ const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
1756
+ const outer_bindings = all_helper_bindings.filter((b) => !loop_scoped_names.has(b.name));
1757
+ const loop_bindings = all_helper_bindings.filter((b) => loop_scoped_names.has(b.name));
1758
+
1759
+ const helper_id = create_generated_identifier(
1760
+ create_local_statement_component_name(transform_context),
1761
+ );
1762
+
1763
+ const outer_aliases = outer_bindings.map((binding) =>
1764
+ create_helper_type_alias_declaration(helper_id, binding),
1765
+ );
1766
+ const loop_aliases = loop_bindings.map((binding) =>
1767
+ create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params),
1768
+ );
1769
+
1770
+ // Synthetic `isLast` prop on the loop helper when there's a tail. It's
1771
+ // passed from the .map callback as `i === source.length - 1` so the loop
1772
+ // helper renders the tail helper only on the last iteration. We do not
1773
+ // gate on this prop's value here — the JSXLogicalExpression appended to
1774
+ // `loop_body` does the gating at render time.
1775
+ const tail_isLast_alias = has_tail
1776
+ ? {
1777
+ id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
1778
+ declaration: b.ts_type_alias(
1779
+ create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
1780
+ b.ts_keyword_type('boolean'),
1781
+ ),
1782
+ }
1783
+ : null;
1784
+
1785
+ const ordered_bindings = [...outer_bindings, ...loop_bindings];
1786
+ const ordered_aliases = [...outer_aliases, ...loop_aliases];
1787
+ const ordered_use_typeof = [...outer_bindings.map(() => true), ...loop_bindings.map(() => false)];
1788
+
1789
+ const signature_bindings = has_tail ? [...ordered_bindings, tail_synthetic_id] : ordered_bindings;
1790
+ const signature_aliases = has_tail
1791
+ ? [...ordered_aliases, /** @type {any} */ (tail_isLast_alias)]
1792
+ : ordered_aliases;
1793
+ const signature_use_typeof = has_tail ? [...ordered_use_typeof, false] : ordered_use_typeof;
1794
+
1795
+ const props_type =
1796
+ signature_bindings.length > 0
1797
+ ? create_helper_props_type_literal_with_typeof_flags(
1798
+ signature_bindings,
1799
+ signature_aliases,
1800
+ signature_use_typeof,
1801
+ )
1802
+ : null;
1803
+ const params =
1804
+ props_type !== null ? [create_typed_helper_props_pattern(signature_bindings, props_type)] : [];
1805
+
1806
+ const fn_saved_bindings = transform_context.available_bindings;
1807
+ transform_context.available_bindings = new Map(fn_saved_bindings);
1808
+ if (has_tail) {
1809
+ transform_context.available_bindings.set(tail_synthetic_id.name, tail_synthetic_id);
1810
+ }
1811
+ const fn_body_statements = build_render_statements(loop_body, true, transform_context);
1812
+ transform_context.available_bindings = fn_saved_bindings;
1813
+
1814
+ const helper_fn = /** @type {any} */ (
1815
+ b.function(clone_identifier(helper_id), params, b.block(fn_body_statements))
1816
+ );
1817
+ helper_fn.metadata = { path: [], is_component: true, is_method: false };
1818
+
1819
+ let helper_decl;
1820
+ if (transform_context.helper_state) {
1821
+ const cache_id = create_generated_identifier(
1822
+ `${transform_context.helper_state.base_name}__${helper_id.name}`,
1823
+ );
1824
+ transform_context.helper_state.helpers.push(create_helper_cache_declaration(cache_id));
1825
+ helper_decl = create_cached_helper_declaration(
1826
+ helper_id,
1827
+ cache_id,
1828
+ create_helper_init_expression(helper_id, helper_fn, node, transform_context),
1829
+ );
1830
+ } else {
1831
+ helper_decl = create_helper_declaration(helper_id, helper_fn, node, transform_context);
1832
+ }
1833
+
1834
+ transform_context.available_bindings = saved_bindings;
1835
+
1836
+ const callback_invocation_element = create_helper_component_element(
1837
+ helper_id,
1838
+ ordered_bindings,
1839
+ node,
1840
+ { mapWrapper: false, mapBindingNames: false, mapBindingValues: false },
1841
+ );
1842
+
1843
+ // When there's a tail, the .map callback always needs an index to compute
1844
+ // `isLast`. If the user didn't write `index i`, synthesize one. The same
1845
+ // identifier is also used as the implicit key fallback below.
1846
+ let index_identifier;
1847
+ if (loop_params.length >= 2) {
1848
+ index_identifier = clone_identifier(loop_params[1]);
1849
+ } else if (has_tail) {
1850
+ index_identifier = create_generated_identifier('i');
1851
+ } else {
1852
+ index_identifier = null;
1853
+ }
1854
+
1855
+ const body_key_expression = find_key_expression_in_body(loop_body);
1856
+ const explicit_key_expression =
1857
+ body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
1858
+ const key_expression =
1859
+ explicit_key_expression ??
1860
+ (loop_params.length >= 2 ? clone_identifier(loop_params[1]) : undefined);
1861
+ if (key_expression) {
1862
+ callback_invocation_element.openingElement.attributes.push(
1863
+ b.jsx_attribute(b.jsx_id('key'), to_jsx_expression_container(key_expression, key_expression)),
1864
+ );
1865
+ }
1866
+
1867
+ if (has_tail && index_identifier) {
1868
+ const length_minus_one = b.binary(
1869
+ '-',
1870
+ b.member(clone_identifier(source_id), 'length'),
1871
+ b.literal(1),
1872
+ );
1873
+ callback_invocation_element.openingElement.attributes.push(
1874
+ b.jsx_attribute(
1875
+ b.jsx_id(tail_synthetic_id.name),
1876
+ to_jsx_expression_container(
1877
+ b.binary('===', clone_identifier(index_identifier), length_minus_one),
1878
+ ),
1879
+ ),
1880
+ );
1881
+ }
1882
+
1883
+ const callback_params =
1884
+ has_tail && loop_params.length < 2 && index_identifier
1885
+ ? [
1886
+ ...loop_params.map((/** @type {any} */ p) => clone_identifier(p)),
1887
+ clone_identifier(index_identifier),
1888
+ ]
1889
+ : loop_params.map((/** @type {any} */ p) => clone_identifier(p));
1890
+
1891
+ const iter_callback = b.arrow(callback_params, callback_invocation_element);
1892
+
1893
+ const map_call = b.call(b.member(clone_identifier(source_id), 'map'), iter_callback);
1894
+
1895
+ // jsx_child for the iteration. When there's a tail, also render the tail
1896
+ // helper directly when the source is empty (no iterations means the loop
1897
+ // helper never fires, so the tail wouldn't run otherwise).
1898
+ const jsx_child = has_tail
1899
+ ? to_jsx_expression_container(
1900
+ b.conditional(
1901
+ b.binary('===', b.member(clone_identifier(source_id), 'length'), b.literal(0)),
1902
+ clone_tail_invocation(/** @type {any} */ (tail_helper)),
1903
+ map_call,
1904
+ ),
1905
+ node,
1906
+ )
1907
+ : to_jsx_expression_container(map_call, node);
1908
+
1909
+ const hoist_statements = [source_decl, source_normalize_decl];
1910
+ if (has_tail) {
1911
+ // TailHelper's setup statements (its alias consts and cache decl).
1912
+ hoist_statements.push(.../** @type {any} */ (tail_helper).setup_statements);
1913
+ }
1914
+ for (const alias of ordered_aliases) hoist_statements.push(alias.declaration);
1915
+ if (has_tail && tail_isLast_alias) {
1916
+ hoist_statements.push(tail_isLast_alias.declaration);
1917
+ }
1918
+ hoist_statements.push(helper_decl);
1919
+
1920
+ return {
1921
+ hoist_statements,
1922
+ jsx_child,
1923
+ };
1924
+ }
1925
+
1926
+ /**
1927
+ * Build a TS `type` alias for a loop-scoped binding, deriving the type
1928
+ * from the iteration source. For the value param we use
1929
+ * `(typeof source)[number]`, which gives the right element type for arrays
1930
+ * and tuples (the common case in JSX templates). For the index param,
1931
+ * the type is always `number`.
1932
+ *
1933
+ * @param {AST.Identifier} helper_id
1934
+ * @param {AST.Identifier} binding
1935
+ * @param {AST.Identifier} source_id
1936
+ * @param {any[]} loop_params
1937
+ * @returns {{ id: AST.Identifier, declaration: any }}
1938
+ */
1939
+ function create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params) {
1940
+ const alias_id = create_generated_identifier(`_tsrx_${helper_id.name}_${binding.name}`);
1941
+ const is_index = loop_params.length > 1 && binding.name === loop_params[1].name;
1942
+ const type_annotation = is_index
1943
+ ? b.ts_keyword_type('number')
1944
+ : /** @type {any} */ ({
1945
+ type: 'TSIndexedAccessType',
1946
+ objectType: b.ts_type_query(clone_identifier(source_id)),
1947
+ indexType: b.ts_keyword_type('number'),
1948
+ metadata: { path: [] },
1949
+ });
1950
+
1951
+ return {
1952
+ id: alias_id,
1953
+ declaration: b.ts_type_alias(clone_identifier(alias_id), type_annotation),
1954
+ };
1955
+ }
1956
+
1957
+ /**
1958
+ * Variant of {@link create_helper_props_type_literal} that lets each
1959
+ * binding's type reference the alias either via `typeof <alias>` (for
1960
+ * outer-scope const aliases) or directly as `<alias>` (for TS `type`
1961
+ * aliases derived from a loop source).
1962
+ *
1963
+ * @param {AST.Identifier[]} bindings
1964
+ * @param {{ id: AST.Identifier }[]} aliases
1965
+ * @param {boolean[]} use_typeof
1966
+ * @returns {any}
1967
+ */
1968
+ function create_helper_props_type_literal_with_typeof_flags(bindings, aliases, use_typeof) {
1969
+ return b.ts_type_literal(
1970
+ bindings.map((binding, i) => {
1971
+ const alias_ref = use_typeof[i]
1972
+ ? b.ts_type_query(clone_identifier(aliases[i].id))
1973
+ : b.ts_type_reference(clone_identifier(aliases[i].id));
1974
+ return b.ts_property_signature(
1975
+ create_generated_identifier(binding.name),
1976
+ b.ts_type_annotation(alias_ref),
1977
+ );
1978
+ }),
1979
+ );
1980
+ }
1981
+
1323
1982
  /**
1324
1983
  * @param {any} node
1325
1984
  * @param {any[]} continuation_body
@@ -1550,47 +2209,26 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
1550
2209
  (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
1551
2210
  );
1552
2211
 
1553
- /** @type {ESTreeJSX.JSXOpeningElement} */
1554
- const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
1555
- has_unmappable_attribute
1556
- ? {
1557
- type: 'JSXOpeningElement',
1558
- name,
1559
- attributes,
1560
- selfClosing,
1561
- metadata: { path: [] },
1562
- }
1563
- : set_loc(
1564
- /** @type {any} */ ({
1565
- type: 'JSXOpeningElement',
1566
- name,
1567
- attributes,
1568
- selfClosing,
1569
- }),
1570
- node.openingElement || node,
1571
- )
2212
+ const opening_element_node = b.jsx_opening_element(
2213
+ name,
2214
+ attributes,
2215
+ selfClosing,
2216
+ node.openingElement?.typeArguments,
1572
2217
  );
2218
+ const openingElement = has_unmappable_attribute
2219
+ ? opening_element_node
2220
+ : set_loc(opening_element_node, node.openingElement || node);
1573
2221
 
1574
- /** @type {ESTreeJSX.JSXClosingElement | null} */
1575
2222
  const closingElement = selfClosing
1576
2223
  ? null
1577
2224
  : set_loc(
1578
- /** @type {any} */ ({
1579
- type: 'JSXClosingElement',
1580
- name: clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
1581
- }),
2225
+ b.jsx_closing_element(
2226
+ clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
2227
+ ),
1582
2228
  node.closingElement || node,
1583
2229
  );
1584
2230
 
1585
- return set_loc(
1586
- /** @type {any} */ ({
1587
- type: 'JSXElement',
1588
- openingElement,
1589
- closingElement,
1590
- children,
1591
- }),
1592
- node,
1593
- );
2231
+ return set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
1594
2232
  }
1595
2233
 
1596
2234
  /**
@@ -1795,12 +2433,22 @@ function get_referenced_helper_bindings(body_nodes, available_bindings) {
1795
2433
  * @param {any} key_expression
1796
2434
  * @param {any} source_node
1797
2435
  * @param {TransformContext} transform_context
2436
+ * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
2437
+ * Used by the switch lift's chained-call build, which allocates ids in
2438
+ * source order in a forward pass and then constructs helpers in reverse so
2439
+ * each fall-through case can reference the next case's component element.
1798
2440
  * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
1799
2441
  */
1800
- function create_hook_safe_helper(body_nodes, key_expression, source_node, transform_context) {
1801
- const helper_id = create_generated_identifier(
1802
- create_local_statement_component_name(transform_context),
1803
- );
2442
+ function create_hook_safe_helper(
2443
+ body_nodes,
2444
+ key_expression,
2445
+ source_node,
2446
+ transform_context,
2447
+ preallocated_helper_id,
2448
+ ) {
2449
+ const helper_id =
2450
+ preallocated_helper_id ??
2451
+ create_generated_identifier(create_local_statement_component_name(transform_context));
1804
2452
  const helper_bindings = get_referenced_helper_bindings(
1805
2453
  body_nodes,
1806
2454
  transform_context.available_bindings,
@@ -2701,27 +3349,10 @@ function try_statement_to_jsx_child(node, transform_context) {
2701
3349
  * @returns {any}
2702
3350
  */
2703
3351
  function create_jsx_element(tag_name, attributes, children) {
2704
- const name = { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } };
2705
- return {
2706
- type: 'JSXElement',
2707
- openingElement: {
2708
- type: 'JSXOpeningElement',
2709
- name,
2710
- attributes,
2711
- selfClosing: children.length === 0,
2712
- metadata: { path: [] },
2713
- },
2714
- closingElement:
2715
- children.length > 0
2716
- ? {
2717
- type: 'JSXClosingElement',
2718
- name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
2719
- metadata: { path: [] },
2720
- }
2721
- : null,
2722
- children,
2723
- metadata: { path: [] },
2724
- };
3352
+ const self_closing = children.length === 0;
3353
+ const opening_element = b.jsx_opening_element(b.jsx_id(tag_name), attributes, self_closing);
3354
+ const closing_element = self_closing ? null : b.jsx_closing_element(b.jsx_id(tag_name));
3355
+ return b.jsx_element_fresh(opening_element, closing_element, children);
2725
3356
  }
2726
3357
 
2727
3358
  /**
@@ -3365,25 +3996,11 @@ function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
3365
3996
  const children = create_element_children(node.children || [], transform_context);
3366
3997
  const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
3367
3998
 
3368
- return /** @type {any} */ ({
3369
- type: 'JSXElement',
3370
- openingElement: {
3371
- type: 'JSXOpeningElement',
3372
- name,
3373
- attributes,
3374
- selfClosing,
3375
- metadata: { path: [] },
3376
- },
3377
- closingElement: selfClosing
3378
- ? null
3379
- : {
3380
- type: 'JSXClosingElement',
3381
- name: clone_jsx_name(name),
3382
- metadata: { path: [] },
3383
- },
3999
+ return b.jsx_element_fresh(
4000
+ b.jsx_opening_element(name, attributes, selfClosing),
4001
+ selfClosing ? null : b.jsx_closing_element(clone_jsx_name(name)),
3384
4002
  children,
3385
- metadata: { path: [] },
3386
- });
4003
+ );
3387
4004
  }
3388
4005
 
3389
4006
  /**
@@ -653,8 +653,11 @@ export function convert_source_map_to_mappings(
653
653
  // Nothing to visit (just source string)
654
654
  return;
655
655
  } else if (node.type === 'JSXOpeningElement') {
656
- // Visit name and attributes in source order
656
+ // Visit name, type arguments, and attributes in source order
657
657
  visit(node.name);
658
+ if (node.typeArguments) {
659
+ visit(node.typeArguments);
660
+ }
658
661
  for (const attr of node.attributes) {
659
662
  visit(attr);
660
663
  }
@@ -1100,6 +1100,82 @@ export function jsx_attribute(name, value = null, shorthand = false, loc_info) {
1100
1100
  return set_location(node, loc_info);
1101
1101
  }
1102
1102
 
1103
+ /**
1104
+ * Build a fresh `JSXOpeningElement`. For elements derived from an existing
1105
+ * Element node, prefer `jsx_element` which spreads from the source.
1106
+ *
1107
+ * @param {ESTreeJSX.JSXOpeningElement['name']} name
1108
+ * @param {ESTreeJSX.JSXOpeningElement['attributes']} [attributes]
1109
+ * @param {boolean} [self_closing]
1110
+ * @param {ESTreeJSX.JSXOpeningElement['typeArguments']} [type_arguments]
1111
+ * @param {AST.NodeWithLocation} [loc_info]
1112
+ * @returns {ESTreeJSX.JSXOpeningElement}
1113
+ */
1114
+ export function jsx_opening_element(
1115
+ name,
1116
+ attributes = [],
1117
+ self_closing = false,
1118
+ type_arguments = undefined,
1119
+ loc_info,
1120
+ ) {
1121
+ const node = /** @type {ESTreeJSX.JSXOpeningElement} */ ({
1122
+ type: 'JSXOpeningElement',
1123
+ name,
1124
+ attributes,
1125
+ selfClosing: self_closing,
1126
+ typeArguments: type_arguments,
1127
+ metadata: { path: [] },
1128
+ });
1129
+
1130
+ return set_location(node, loc_info);
1131
+ }
1132
+
1133
+ /**
1134
+ * Build a fresh `JSXClosingElement`.
1135
+ *
1136
+ * @param {ESTreeJSX.JSXClosingElement['name']} name
1137
+ * @param {AST.NodeWithLocation} [loc_info]
1138
+ * @returns {ESTreeJSX.JSXClosingElement}
1139
+ */
1140
+ export function jsx_closing_element(name, loc_info) {
1141
+ const node = /** @type {ESTreeJSX.JSXClosingElement} */ ({
1142
+ type: 'JSXClosingElement',
1143
+ name,
1144
+ metadata: { path: [] },
1145
+ });
1146
+
1147
+ return set_location(node, loc_info);
1148
+ }
1149
+
1150
+ /**
1151
+ * Build a fresh `JSXElement` from explicit opening / closing / children.
1152
+ * Companion to `jsx_opening_element` / `jsx_closing_element`. For elements
1153
+ * derived from an existing source node, use `jsx_element` (which spreads
1154
+ * the source's name and metadata).
1155
+ *
1156
+ * @param {ESTreeJSX.JSXOpeningElement} opening_element
1157
+ * @param {ESTreeJSX.JSXClosingElement | null} [closing_element]
1158
+ * @param {ESTreeJSX.JSXElement['children']} [children]
1159
+ * @param {AST.NodeWithLocation} [loc_info]
1160
+ * @returns {ESTreeJSX.JSXElement}
1161
+ */
1162
+ export function jsx_element_fresh(
1163
+ opening_element,
1164
+ closing_element = null,
1165
+ children = [],
1166
+ loc_info,
1167
+ ) {
1168
+ const node = /** @type {ESTreeJSX.JSXElement} */ ({
1169
+ type: 'JSXElement',
1170
+ openingElement: opening_element,
1171
+ closingElement: closing_element,
1172
+ children,
1173
+ metadata: { path: [] },
1174
+ });
1175
+
1176
+ return set_location(node, loc_info);
1177
+ }
1178
+
1103
1179
  /**
1104
1180
  * @param {AST.Element} node
1105
1181
  * @param {ESTreeJSX.JSXOpeningElement['attributes']} attributes