@tsrx/core 0.0.19 → 0.0.21

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.
@@ -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,
@@ -92,6 +93,7 @@ export function createJsxTransform(platform) {
92
93
  const module_uses_server_directive = should_scan_use_server_directive
93
94
  ? has_use_server_directive(ast)
94
95
  : true;
96
+ const collect = !!(options?.collect || options?.loose);
95
97
  /** @type {any[]} */
96
98
  const stylesheets = [];
97
99
 
@@ -107,8 +109,8 @@ export function createJsxTransform(platform) {
107
109
  lazy_next_id: 0,
108
110
  current_css_hash: null,
109
111
  filename: filename ?? null,
110
- loose: !!options?.loose,
111
- errors: options?.loose ? options?.errors : undefined,
112
+ collect,
113
+ errors: collect ? options?.errors : undefined,
112
114
  comments: options?.comments,
113
115
  // Platforms can seed their own tracking state (e.g. solid's
114
116
  // needs_show / needs_for flags) via `hooks.initialState`.
@@ -120,7 +122,12 @@ export function createJsxTransform(platform) {
120
122
  walk(/** @type {any} */ (ast), transform_context, {
121
123
  ReturnStatement(node, { next, path }) {
122
124
  if (get_component_from_path(path)) {
123
- validate_component_return_statement(node, filename);
125
+ validate_component_return_statement(
126
+ node,
127
+ filename,
128
+ transform_context.errors,
129
+ transform_context.comments,
130
+ );
124
131
  }
125
132
 
126
133
  return next();
@@ -583,6 +590,105 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
583
590
  continue;
584
591
  }
585
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
+
586
692
  if (is_jsx_child(child)) {
587
693
  const jsx = to_jsx_child(child, transform_context);
588
694
  if (interleaved && is_capturable_jsx_child(jsx)) {
@@ -832,35 +938,26 @@ function create_helper_props_property(binding) {
832
938
  */
833
939
  function create_helper_component_element(helper_id, bindings, source_node, mapping = {}) {
834
940
  const { mapWrapper = true, mapBindingNames = true, mapBindingValues = true } = mapping;
835
- const attributes = bindings.map(
836
- (binding) =>
837
- /** @type {any} */ ({
838
- type: 'JSXAttribute',
839
- name: identifier_to_jsx_name(
840
- mapBindingNames ? clone_identifier(binding) : create_generated_identifier(binding.name),
841
- ),
842
- value: to_jsx_expression_container(
843
- mapBindingValues ? clone_identifier(binding) : create_generated_identifier(binding.name),
844
- binding,
845
- ),
846
- metadata: { path: [] },
847
- }),
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
+ ),
848
951
  );
849
952
 
850
- const openingElement = {
851
- type: 'JSXOpeningElement',
852
- 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)),
853
955
  attributes,
854
- selfClosing: true,
855
- metadata: { path: [] },
856
- };
857
- const element = /** @type {any} */ ({
858
- type: 'JSXElement',
859
- openingElement: mapWrapper ? set_loc(openingElement, source_node) : openingElement,
860
- closingElement: null,
861
- children: [],
862
- metadata: { path: [] },
863
- });
956
+ true,
957
+ );
958
+ const element = b.jsx_element_fresh(
959
+ mapWrapper ? set_loc(opening_element, source_node) : opening_element,
960
+ );
864
961
 
865
962
  return mapWrapper ? set_loc(element, source_node) : element;
866
963
  }
@@ -1050,21 +1147,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1050
1147
  const name = create_helper_name(transform_context.helper_state, 'static');
1051
1148
  const id = create_generated_identifier(name);
1052
1149
 
1053
- transform_context.helper_state.statics.push(
1054
- /** @type {any} */ ({
1055
- type: 'VariableDeclaration',
1056
- kind: 'const',
1057
- declarations: [
1058
- {
1059
- type: 'VariableDeclarator',
1060
- id,
1061
- init: node,
1062
- metadata: { path: [] },
1063
- },
1064
- ],
1065
- metadata: { path: [] },
1066
- }),
1067
- );
1150
+ transform_context.helper_state.statics.push(b.const(id, node));
1068
1151
 
1069
1152
  render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
1070
1153
  }
@@ -1170,25 +1253,13 @@ function create_component_return_statement(
1170
1253
  source_node,
1171
1254
  map_render_node_locations = true,
1172
1255
  ) {
1173
- return set_loc(
1174
- /** @type {any} */ ({
1175
- type: 'ReturnStatement',
1176
- argument: build_return_expression(
1177
- render_nodes.map((node) =>
1178
- map_render_node_locations
1179
- ? clone_expression_node(node)
1180
- : clone_expression_node_without_locations(node),
1181
- ),
1182
- ) || {
1183
- type: 'Literal',
1184
- value: null,
1185
- raw: 'null',
1186
- metadata: { path: [] },
1187
- },
1188
- metadata: { path: [] },
1189
- }),
1190
- 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),
1191
1260
  );
1261
+
1262
+ return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
1192
1263
  }
1193
1264
 
1194
1265
  /**
@@ -1200,20 +1271,14 @@ function create_component_lone_return_if_statement(node, render_nodes) {
1200
1271
  const consequent_body = get_if_consequent_body(node);
1201
1272
 
1202
1273
  return set_loc(
1203
- /** @type {any} */ ({
1204
- type: 'IfStatement',
1205
- test: node.test,
1206
- consequent: set_loc(
1207
- /** @type {any} */ ({
1208
- type: 'BlockStatement',
1209
- body: [create_component_return_statement(render_nodes, consequent_body[0], false)],
1210
- metadata: { path: [] },
1211
- }),
1274
+ b.if(
1275
+ node.test,
1276
+ set_loc(
1277
+ b.block([create_component_return_statement(render_nodes, consequent_body[0], false)]),
1212
1278
  node.consequent,
1213
1279
  ),
1214
- alternate: null,
1215
- metadata: { path: [] },
1216
- }),
1280
+ null,
1281
+ ),
1217
1282
  node,
1218
1283
  );
1219
1284
  }
@@ -1229,23 +1294,84 @@ function create_component_returning_if_statement(node, render_nodes, transform_c
1229
1294
  const branch_statements = build_render_statements(consequent_body, true, transform_context);
1230
1295
  prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
1231
1296
 
1232
- return set_loc(
1233
- /** @type {any} */ ({
1234
- type: 'IfStatement',
1235
- test: node.test,
1236
- consequent: set_loc(
1237
- /** @type {any} */ ({
1238
- type: 'BlockStatement',
1239
- body: branch_statements,
1240
- metadata: { path: [] },
1241
- }),
1242
- node.consequent,
1243
- ),
1244
- alternate: null,
1245
- metadata: { path: [] },
1246
- }),
1247
- node,
1248
- );
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 };
1249
1375
  }
1250
1376
 
1251
1377
  /**
@@ -1277,43 +1403,582 @@ function create_component_helper_split_returning_if_statements(
1277
1403
  node,
1278
1404
  transform_context,
1279
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
+
1280
1415
  return [
1281
- set_loc(
1282
- /** @type {any} */ ({
1283
- type: 'IfStatement',
1284
- test: node.test,
1285
- consequent: set_loc(
1286
- /** @type {any} */ ({
1287
- type: 'BlockStatement',
1288
- body: [
1289
- ...branch_helper.setup_statements,
1290
- {
1291
- type: 'ReturnStatement',
1292
- argument: combine_render_return_argument(
1293
- render_nodes,
1294
- branch_helper.component_element,
1295
- ),
1296
- metadata: { path: [] },
1297
- },
1298
- ],
1299
- metadata: { path: [] },
1300
- }),
1301
- node.consequent,
1302
- ),
1303
- alternate: null,
1304
- metadata: { path: [] },
1305
- }),
1306
- node,
1307
- ),
1416
+ set_loc(b.if(node.test, branch_block, null), node),
1308
1417
  ...continuation_helper.setup_statements,
1309
- {
1310
- type: 'ReturnStatement',
1311
- argument: combine_render_return_argument(render_nodes, continuation_helper.component_element),
1312
- 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, []);
1313
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),
1314
1669
  ];
1315
1670
  }
1316
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
+
1317
1982
  /**
1318
1983
  * @param {any} node
1319
1984
  * @param {any[]} continuation_body
@@ -1544,47 +2209,21 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
1544
2209
  (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
1545
2210
  );
1546
2211
 
1547
- /** @type {ESTreeJSX.JSXOpeningElement} */
1548
- const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
1549
- has_unmappable_attribute
1550
- ? {
1551
- type: 'JSXOpeningElement',
1552
- name,
1553
- attributes,
1554
- selfClosing,
1555
- metadata: { path: [] },
1556
- }
1557
- : set_loc(
1558
- /** @type {any} */ ({
1559
- type: 'JSXOpeningElement',
1560
- name,
1561
- attributes,
1562
- selfClosing,
1563
- }),
1564
- node.openingElement || node,
1565
- )
1566
- );
2212
+ const opening_element_node = b.jsx_opening_element(name, attributes, selfClosing);
2213
+ const openingElement = has_unmappable_attribute
2214
+ ? opening_element_node
2215
+ : set_loc(opening_element_node, node.openingElement || node);
1567
2216
 
1568
- /** @type {ESTreeJSX.JSXClosingElement | null} */
1569
2217
  const closingElement = selfClosing
1570
2218
  ? null
1571
2219
  : set_loc(
1572
- /** @type {any} */ ({
1573
- type: 'JSXClosingElement',
1574
- name: clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
1575
- }),
2220
+ b.jsx_closing_element(
2221
+ clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
2222
+ ),
1576
2223
  node.closingElement || node,
1577
2224
  );
1578
2225
 
1579
- return set_loc(
1580
- /** @type {any} */ ({
1581
- type: 'JSXElement',
1582
- openingElement,
1583
- closingElement,
1584
- children,
1585
- }),
1586
- node,
1587
- );
2226
+ return set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
1588
2227
  }
1589
2228
 
1590
2229
  /**
@@ -1789,12 +2428,22 @@ function get_referenced_helper_bindings(body_nodes, available_bindings) {
1789
2428
  * @param {any} key_expression
1790
2429
  * @param {any} source_node
1791
2430
  * @param {TransformContext} transform_context
2431
+ * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
2432
+ * Used by the switch lift's chained-call build, which allocates ids in
2433
+ * source order in a forward pass and then constructs helpers in reverse so
2434
+ * each fall-through case can reference the next case's component element.
1792
2435
  * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
1793
2436
  */
1794
- function create_hook_safe_helper(body_nodes, key_expression, source_node, transform_context) {
1795
- const helper_id = create_generated_identifier(
1796
- create_local_statement_component_name(transform_context),
1797
- );
2437
+ function create_hook_safe_helper(
2438
+ body_nodes,
2439
+ key_expression,
2440
+ source_node,
2441
+ transform_context,
2442
+ preallocated_helper_id,
2443
+ ) {
2444
+ const helper_id =
2445
+ preallocated_helper_id ??
2446
+ create_generated_identifier(create_local_statement_component_name(transform_context));
1798
2447
  const helper_bindings = get_referenced_helper_bindings(
1799
2448
  body_nodes,
1800
2449
  transform_context.available_bindings,
@@ -2695,27 +3344,10 @@ function try_statement_to_jsx_child(node, transform_context) {
2695
3344
  * @returns {any}
2696
3345
  */
2697
3346
  function create_jsx_element(tag_name, attributes, children) {
2698
- const name = { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } };
2699
- return {
2700
- type: 'JSXElement',
2701
- openingElement: {
2702
- type: 'JSXOpeningElement',
2703
- name,
2704
- attributes,
2705
- selfClosing: children.length === 0,
2706
- metadata: { path: [] },
2707
- },
2708
- closingElement:
2709
- children.length > 0
2710
- ? {
2711
- type: 'JSXClosingElement',
2712
- name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
2713
- metadata: { path: [] },
2714
- }
2715
- : null,
2716
- children,
2717
- metadata: { path: [] },
2718
- };
3347
+ const self_closing = children.length === 0;
3348
+ const opening_element = b.jsx_opening_element(b.jsx_id(tag_name), attributes, self_closing);
3349
+ const closing_element = self_closing ? null : b.jsx_closing_element(b.jsx_id(tag_name));
3350
+ return b.jsx_element_fresh(opening_element, closing_element, children);
2719
3351
  }
2720
3352
 
2721
3353
  /**
@@ -3049,8 +3681,8 @@ export function validate_at_most_one_ref_attribute(raw_attrs, transform_context)
3049
3681
  }
3050
3682
  for (let i = 0; i < refs.length; i++) {
3051
3683
  const node = refs[i];
3052
- if (!transform_context?.loose && i === 0) {
3053
- // in the non-loose mode, only throw on the second duplicate
3684
+ if (!transform_context?.collect && i === 0) {
3685
+ // when not collecting, only throw on the second duplicate
3054
3686
  continue;
3055
3687
  }
3056
3688
  error(
@@ -3359,25 +3991,11 @@ function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
3359
3991
  const children = create_element_children(node.children || [], transform_context);
3360
3992
  const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
3361
3993
 
3362
- return /** @type {any} */ ({
3363
- type: 'JSXElement',
3364
- openingElement: {
3365
- type: 'JSXOpeningElement',
3366
- name,
3367
- attributes,
3368
- selfClosing,
3369
- metadata: { path: [] },
3370
- },
3371
- closingElement: selfClosing
3372
- ? null
3373
- : {
3374
- type: 'JSXClosingElement',
3375
- name: clone_jsx_name(name),
3376
- metadata: { path: [] },
3377
- },
3994
+ return b.jsx_element_fresh(
3995
+ b.jsx_opening_element(name, attributes, selfClosing),
3996
+ selfClosing ? null : b.jsx_closing_element(clone_jsx_name(name)),
3378
3997
  children,
3379
- metadata: { path: [] },
3380
- });
3998
+ );
3381
3999
  }
3382
4000
 
3383
4001
  /**