@tsrx/react 0.0.1

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.
@@ -0,0 +1,2283 @@
1
+ /** @import * as AST from 'estree' */
2
+ /** @import * as ESTreeJSX from 'estree-jsx' */
3
+
4
+ import { walk } from 'zimmerframe';
5
+ import { print } from 'esrap';
6
+ import tsx from 'esrap/languages/tsx';
7
+ import { renderStylesheets, setLocation } from '@tsrx/core';
8
+
9
+ /**
10
+ * @typedef {{
11
+ * local_statement_component_index: number,
12
+ * needs_error_boundary: boolean,
13
+ * needs_suspense: boolean,
14
+ * }} TransformContext
15
+ */
16
+
17
+ /**
18
+ * Transform a parsed tsrx-react AST into a TSX/JSX module.
19
+ *
20
+ * Replaces Ripple-specific `Component`/`Element`/`Text`/`TSRXExpression`
21
+ * nodes with their standard JSX equivalents inside a `FunctionDeclaration`.
22
+ * Any `<style>` element declared inside a component is collected,
23
+ * rendered via `@tsrx/core`'s stylesheet renderer, and returned alongside
24
+ * the JS output so a downstream plugin can inject it. The compiler also
25
+ * augments every non-style Element in a scoped component with the
26
+ * stylesheet's hash class so scoped selectors match correctly.
27
+ *
28
+ * @param {AST.Program} ast
29
+ * @param {string} source
30
+ * @param {string} [filename]
31
+ * @returns {{ ast: AST.Program, code: string, map: any, css: { code: string, hash: string } | null }}
32
+ */
33
+ export function transform(ast, source, filename) {
34
+ /** @type {any[]} */
35
+ const stylesheets = [];
36
+
37
+ /** @type {TransformContext} */
38
+ const transform_context = {
39
+ local_statement_component_index: 0,
40
+ needs_error_boundary: false,
41
+ needs_suspense: false,
42
+ };
43
+
44
+ walk(/** @type {any} */ (ast), transform_context, {
45
+ Component(node, { next, state }) {
46
+ const as_any = /** @type {any} */ (node);
47
+ const css = as_any.css;
48
+ if (css) {
49
+ stylesheets.push(css);
50
+ const hash = css.hash;
51
+ annotate_component_with_hash(as_any, hash);
52
+ }
53
+ return next(state);
54
+ },
55
+ });
56
+
57
+ const transformed = walk(/** @type {any} */ (ast), transform_context, {
58
+ Component(node, { next, state }) {
59
+ const inner = /** @type {any} */ (next() ?? node);
60
+ return /** @type {any} */ (component_to_function_declaration(inner, state));
61
+ },
62
+
63
+ Tsx(node, { next }) {
64
+ const inner = /** @type {any} */ (next() ?? node);
65
+ return /** @type {any} */ (tsx_node_to_jsx_expression(inner));
66
+ },
67
+
68
+ TsxCompat(node, { next }) {
69
+ const inner = /** @type {any} */ (next() ?? node);
70
+ return /** @type {any} */ (tsx_compat_node_to_jsx_expression(inner));
71
+ },
72
+
73
+ Element(node, { next, state }) {
74
+ const inner = /** @type {any} */ (next() ?? node);
75
+ return /** @type {any} */ (to_jsx_element(inner, state));
76
+ },
77
+
78
+ Text(node, { next }) {
79
+ const inner = /** @type {any} */ (next() ?? node);
80
+ return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
81
+ },
82
+
83
+ TSRXExpression(node, { next }) {
84
+ const inner = /** @type {any} */ (next() ?? node);
85
+ return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
86
+ },
87
+ });
88
+
89
+ const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
90
+ inject_try_imports(expanded, transform_context);
91
+
92
+ const result = print(/** @type {any} */ (expanded), tsx(), {
93
+ sourceMapSource: filename,
94
+ sourceMapContent: source,
95
+ });
96
+
97
+ const css =
98
+ stylesheets.length > 0
99
+ ? {
100
+ code: renderStylesheets(
101
+ /** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
102
+ ),
103
+ hash: stylesheets.map((s) => s.hash).join(' '),
104
+ }
105
+ : null;
106
+
107
+ return { ast: expanded, code: result.code, map: result.map, css };
108
+ }
109
+
110
+ /**
111
+ * @param {any} component
112
+ * @param {TransformContext} transform_context
113
+ * @returns {AST.FunctionDeclaration}
114
+ */
115
+ function component_to_function_declaration(component, transform_context) {
116
+ const helper_state = create_helper_state(component.id?.name || 'Component');
117
+ const fn = /** @type {any} */ ({
118
+ type: 'FunctionDeclaration',
119
+ id: component.id,
120
+ params: component.params || [],
121
+ body: {
122
+ type: 'BlockStatement',
123
+ body: build_component_statements(
124
+ /** @type {any[]} */ (component.body),
125
+ helper_state,
126
+ collect_param_bindings(component.params || []),
127
+ transform_context,
128
+ ),
129
+ metadata: { path: [] },
130
+ },
131
+ async: false,
132
+ generator: false,
133
+ metadata: {
134
+ path: [],
135
+ is_component: true,
136
+ is_method: true,
137
+ },
138
+ });
139
+
140
+ fn.metadata.generated_helpers = helper_state.helpers;
141
+
142
+ setLocation(fn, /** @type {any} */ (component), true);
143
+ return fn;
144
+ }
145
+
146
+ /**
147
+ * @param {any[]} body_nodes
148
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
149
+ * @param {Map<string, AST.Identifier>} available_bindings
150
+ * @param {TransformContext} transform_context
151
+ * @returns {any[]}
152
+ */
153
+ function build_component_statements(
154
+ body_nodes,
155
+ helper_state,
156
+ available_bindings,
157
+ transform_context,
158
+ ) {
159
+ const split_index = find_hook_safe_split_index(body_nodes);
160
+ if (split_index === -1) {
161
+ return build_render_statements(body_nodes, false, transform_context);
162
+ }
163
+
164
+ const statements = [];
165
+ const render_nodes = [];
166
+ const bindings = new Map(available_bindings);
167
+
168
+ for (let i = 0; i < split_index; i += 1) {
169
+ const child = body_nodes[i];
170
+
171
+ if (is_bare_return_statement(child)) {
172
+ statements.push(create_component_return_statement(render_nodes, child));
173
+ return statements;
174
+ }
175
+
176
+ if (is_lone_return_if_statement(child)) {
177
+ statements.push(create_component_lone_return_if_statement(child, render_nodes));
178
+ continue;
179
+ }
180
+
181
+ if (is_jsx_child(child)) {
182
+ render_nodes.push(to_jsx_child(child, transform_context));
183
+ } else {
184
+ statements.push(child);
185
+ collect_statement_bindings(child, bindings);
186
+ }
187
+ }
188
+
189
+ const split_node = body_nodes[split_index];
190
+ const consequent_body =
191
+ split_node.consequent.type === 'BlockStatement'
192
+ ? split_node.consequent.body
193
+ : [split_node.consequent];
194
+ const short_branch_body = consequent_body.filter(
195
+ (/** @type {any} */ child) => !is_bare_return_statement(child),
196
+ );
197
+ const continuation_body = body_nodes.slice(split_index + 1);
198
+ const short_branch = create_helper_component_expression(
199
+ short_branch_body,
200
+ helper_state,
201
+ bindings,
202
+ split_node.consequent,
203
+ 'Exit',
204
+ transform_context,
205
+ );
206
+ const continuation = create_helper_component_expression(
207
+ continuation_body,
208
+ helper_state,
209
+ bindings,
210
+ split_node,
211
+ 'Continue',
212
+ transform_context,
213
+ );
214
+
215
+ render_nodes.push(
216
+ to_jsx_expression_container(
217
+ set_loc(
218
+ /** @type {any} */ ({
219
+ type: 'ConditionalExpression',
220
+ test: split_node.test,
221
+ consequent: short_branch,
222
+ alternate: continuation,
223
+ metadata: { path: [] },
224
+ }),
225
+ split_node,
226
+ ),
227
+ split_node,
228
+ ),
229
+ );
230
+
231
+ statements.push(create_component_return_statement(render_nodes, split_node));
232
+ return statements;
233
+ }
234
+
235
+ /**
236
+ * @param {any[]} body_nodes
237
+ * @param {boolean} return_null_when_empty
238
+ * @param {TransformContext} transform_context
239
+ * @returns {any[]}
240
+ */
241
+ function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
242
+ const statements = [];
243
+ const render_nodes = [];
244
+
245
+ for (const child of body_nodes) {
246
+ if (is_bare_return_statement(child)) {
247
+ statements.push(create_component_return_statement(render_nodes, child));
248
+ return statements;
249
+ }
250
+
251
+ if (is_lone_return_if_statement(child)) {
252
+ statements.push(create_component_lone_return_if_statement(child, render_nodes));
253
+ continue;
254
+ }
255
+
256
+ if (is_jsx_child(child)) {
257
+ render_nodes.push(to_jsx_child(child, transform_context));
258
+ } else {
259
+ statements.push(child);
260
+ }
261
+ }
262
+
263
+ const return_arg = build_return_expression(render_nodes);
264
+ if (return_arg || return_null_when_empty) {
265
+ statements.push({
266
+ type: 'ReturnStatement',
267
+ argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
268
+ });
269
+ }
270
+
271
+ return statements;
272
+ }
273
+
274
+ /**
275
+ * @param {any[]} body_nodes
276
+ * @returns {number}
277
+ */
278
+ function find_hook_safe_split_index(body_nodes) {
279
+ for (let i = 0; i < body_nodes.length; i += 1) {
280
+ if (!is_lone_return_if_statement(body_nodes[i])) {
281
+ continue;
282
+ }
283
+
284
+ if (body_contains_top_level_hook_call(body_nodes.slice(i + 1))) {
285
+ return i;
286
+ }
287
+ }
288
+
289
+ return -1;
290
+ }
291
+
292
+ /**
293
+ * @param {any[]} body_nodes
294
+ * @returns {boolean}
295
+ */
296
+ function body_contains_top_level_hook_call(body_nodes) {
297
+ return body_nodes.some(statement_contains_top_level_hook_call);
298
+ }
299
+
300
+ /**
301
+ * @param {any} node
302
+ * @returns {boolean}
303
+ */
304
+ function statement_contains_top_level_hook_call(node) {
305
+ return node_contains_top_level_hook_call(node, false);
306
+ }
307
+
308
+ /**
309
+ * @param {any} node
310
+ * @param {boolean} inside_nested_function
311
+ * @returns {boolean}
312
+ */
313
+ function node_contains_top_level_hook_call(node, inside_nested_function) {
314
+ if (!node || typeof node !== 'object') {
315
+ return false;
316
+ }
317
+
318
+ if (
319
+ inside_nested_function &&
320
+ (node.type === 'FunctionDeclaration' ||
321
+ node.type === 'FunctionExpression' ||
322
+ node.type === 'ArrowFunctionExpression')
323
+ ) {
324
+ return false;
325
+ }
326
+
327
+ if (
328
+ node.type === 'FunctionDeclaration' ||
329
+ node.type === 'FunctionExpression' ||
330
+ node.type === 'ArrowFunctionExpression'
331
+ ) {
332
+ const next_inside_nested_function = true;
333
+ for (const key of Object.keys(node)) {
334
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
335
+ continue;
336
+ }
337
+ if (node_contains_top_level_hook_call(node[key], next_inside_nested_function)) {
338
+ return true;
339
+ }
340
+ }
341
+ return false;
342
+ }
343
+
344
+ if (!inside_nested_function && node.type === 'CallExpression' && is_hook_callee(node.callee)) {
345
+ return true;
346
+ }
347
+
348
+ if (Array.isArray(node)) {
349
+ return node.some((child) => node_contains_top_level_hook_call(child, inside_nested_function));
350
+ }
351
+
352
+ for (const key of Object.keys(node)) {
353
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
354
+ continue;
355
+ }
356
+ if (node_contains_top_level_hook_call(node[key], inside_nested_function)) {
357
+ return true;
358
+ }
359
+ }
360
+
361
+ return false;
362
+ }
363
+
364
+ /**
365
+ * @param {any} callee
366
+ * @returns {boolean}
367
+ */
368
+ function is_hook_callee(callee) {
369
+ if (!callee) return false;
370
+
371
+ if (callee.type === 'Identifier') {
372
+ return /^use[A-Z0-9]/.test(callee.name);
373
+ }
374
+
375
+ if (
376
+ !callee.computed &&
377
+ callee.type === 'MemberExpression' &&
378
+ callee.property?.type === 'Identifier'
379
+ ) {
380
+ return /^use[A-Z0-9]/.test(callee.property.name);
381
+ }
382
+
383
+ return false;
384
+ }
385
+
386
+ /**
387
+ * @param {any[]} body_nodes
388
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
389
+ * @param {Map<string, AST.Identifier>} available_bindings
390
+ * @param {any} source_node
391
+ * @param {string} suffix
392
+ * @param {TransformContext} transform_context
393
+ * @returns {any}
394
+ */
395
+ function create_helper_component_expression(
396
+ body_nodes,
397
+ helper_state,
398
+ available_bindings,
399
+ source_node,
400
+ suffix,
401
+ transform_context,
402
+ ) {
403
+ if (body_nodes.length === 0) {
404
+ return create_null_literal();
405
+ }
406
+
407
+ const helper_name = create_helper_name(helper_state, suffix);
408
+ const helper_id = set_loc(create_generated_identifier(helper_name), source_node);
409
+ const helper_bindings = Array.from(available_bindings.values());
410
+ const helper_fn = create_helper_function_declaration(
411
+ helper_id,
412
+ body_nodes,
413
+ helper_state,
414
+ available_bindings,
415
+ helper_bindings,
416
+ source_node,
417
+ transform_context,
418
+ );
419
+
420
+ helper_state.helpers.push(helper_fn);
421
+
422
+ return create_helper_component_element(helper_id, helper_bindings, source_node);
423
+ }
424
+
425
+ /**
426
+ * @param {AST.Identifier} helper_id
427
+ * @param {any[]} body_nodes
428
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
429
+ * @param {Map<string, AST.Identifier>} available_bindings
430
+ * @param {AST.Identifier[]} helper_bindings
431
+ * @param {any} source_node
432
+ * @param {TransformContext} transform_context
433
+ * @returns {AST.FunctionDeclaration}
434
+ */
435
+ function create_helper_function_declaration(
436
+ helper_id,
437
+ body_nodes,
438
+ helper_state,
439
+ available_bindings,
440
+ helper_bindings,
441
+ source_node,
442
+ transform_context,
443
+ ) {
444
+ const fn = /** @type {any} */ ({
445
+ type: 'FunctionDeclaration',
446
+ id: helper_id,
447
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
448
+ body: {
449
+ type: 'BlockStatement',
450
+ body: build_component_statements(
451
+ body_nodes,
452
+ helper_state,
453
+ new Map(available_bindings),
454
+ transform_context,
455
+ ),
456
+ metadata: { path: [] },
457
+ },
458
+ async: false,
459
+ generator: false,
460
+ metadata: {
461
+ path: [],
462
+ is_component: true,
463
+ is_method: true,
464
+ },
465
+ });
466
+
467
+ return set_loc(fn, source_node);
468
+ }
469
+
470
+ /**
471
+ * @param {AST.Identifier[]} bindings
472
+ * @returns {AST.ObjectPattern}
473
+ */
474
+ function create_helper_props_pattern(bindings) {
475
+ return /** @type {any} */ ({
476
+ type: 'ObjectPattern',
477
+ properties: bindings.map((binding) => create_helper_props_property(binding)),
478
+ metadata: { path: [] },
479
+ });
480
+ }
481
+
482
+ /**
483
+ * @param {AST.Identifier} binding
484
+ * @returns {AST.Property}
485
+ */
486
+ function create_helper_props_property(binding) {
487
+ const key = clone_identifier(binding);
488
+ const value = clone_identifier(binding);
489
+
490
+ return /** @type {any} */ ({
491
+ type: 'Property',
492
+ key,
493
+ value,
494
+ kind: 'init',
495
+ method: false,
496
+ shorthand: true,
497
+ computed: false,
498
+ metadata: { path: [] },
499
+ });
500
+ }
501
+
502
+ /**
503
+ * @param {AST.Identifier} helper_id
504
+ * @param {AST.Identifier[]} bindings
505
+ * @param {any} source_node
506
+ * @returns {ESTreeJSX.JSXElement}
507
+ */
508
+ function create_helper_component_element(helper_id, bindings, source_node) {
509
+ const attributes = bindings.map(
510
+ (binding) =>
511
+ /** @type {any} */ ({
512
+ type: 'JSXAttribute',
513
+ name: identifier_to_jsx_name(clone_identifier(binding)),
514
+ value: to_jsx_expression_container(clone_identifier(binding), binding),
515
+ metadata: { path: [] },
516
+ }),
517
+ );
518
+
519
+ return set_loc(
520
+ /** @type {any} */ ({
521
+ type: 'JSXElement',
522
+ openingElement: set_loc(
523
+ {
524
+ type: 'JSXOpeningElement',
525
+ name: identifier_to_jsx_name(clone_identifier(helper_id)),
526
+ attributes,
527
+ selfClosing: true,
528
+ metadata: { path: [] },
529
+ },
530
+ source_node,
531
+ ),
532
+ closingElement: null,
533
+ children: [],
534
+ metadata: { path: [] },
535
+ }),
536
+ source_node,
537
+ );
538
+ }
539
+
540
+ /**
541
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
542
+ * @param {string} suffix
543
+ * @returns {string}
544
+ */
545
+ function create_helper_name(helper_state, suffix) {
546
+ helper_state.next_id += 1;
547
+ return `${helper_state.base_name}__${suffix}${helper_state.next_id}`;
548
+ }
549
+
550
+ /**
551
+ * @param {string} base_name
552
+ * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }}
553
+ */
554
+ function create_helper_state(base_name) {
555
+ return {
556
+ base_name,
557
+ next_id: 0,
558
+ helpers: [],
559
+ };
560
+ }
561
+
562
+ /**
563
+ * @param {any[]} params
564
+ * @returns {Map<string, AST.Identifier>}
565
+ */
566
+ function collect_param_bindings(params) {
567
+ const bindings = new Map();
568
+ for (const param of params) {
569
+ collect_pattern_bindings(param, bindings);
570
+ }
571
+ return bindings;
572
+ }
573
+
574
+ /**
575
+ * @param {any} statement
576
+ * @param {Map<string, AST.Identifier>} bindings
577
+ * @returns {void}
578
+ */
579
+ function collect_statement_bindings(statement, bindings) {
580
+ if (!statement) return;
581
+
582
+ if (statement.type === 'VariableDeclaration') {
583
+ for (const declaration of statement.declarations || []) {
584
+ collect_pattern_bindings(declaration.id, bindings);
585
+ }
586
+ return;
587
+ }
588
+
589
+ if (
590
+ (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
591
+ statement.id
592
+ ) {
593
+ bindings.set(statement.id.name, statement.id);
594
+ }
595
+ }
596
+
597
+ /**
598
+ * @param {any} pattern
599
+ * @param {Map<string, AST.Identifier>} bindings
600
+ * @returns {void}
601
+ */
602
+ function collect_pattern_bindings(pattern, bindings) {
603
+ if (!pattern || typeof pattern !== 'object') return;
604
+
605
+ if (pattern.type === 'Identifier') {
606
+ bindings.set(pattern.name, pattern);
607
+ return;
608
+ }
609
+
610
+ if (pattern.type === 'RestElement') {
611
+ collect_pattern_bindings(pattern.argument, bindings);
612
+ return;
613
+ }
614
+
615
+ if (pattern.type === 'AssignmentPattern') {
616
+ collect_pattern_bindings(pattern.left, bindings);
617
+ return;
618
+ }
619
+
620
+ if (pattern.type === 'ArrayPattern') {
621
+ for (const element of pattern.elements || []) {
622
+ collect_pattern_bindings(element, bindings);
623
+ }
624
+ return;
625
+ }
626
+
627
+ if (pattern.type === 'ObjectPattern') {
628
+ for (const property of pattern.properties || []) {
629
+ if (property.type === 'RestElement') {
630
+ collect_pattern_bindings(property.argument, bindings);
631
+ } else {
632
+ collect_pattern_bindings(property.value, bindings);
633
+ }
634
+ }
635
+ }
636
+ }
637
+
638
+ /**
639
+ * @param {AST.Identifier} identifier
640
+ * @returns {AST.Identifier}
641
+ */
642
+ function clone_identifier(identifier) {
643
+ return set_loc(
644
+ /** @type {any} */ ({
645
+ type: 'Identifier',
646
+ name: identifier.name,
647
+ metadata: { path: [] },
648
+ }),
649
+ identifier,
650
+ );
651
+ }
652
+
653
+ /**
654
+ * @returns {AST.Literal}
655
+ */
656
+ function create_null_literal() {
657
+ return /** @type {any} */ ({
658
+ type: 'Literal',
659
+ value: null,
660
+ raw: 'null',
661
+ metadata: { path: [] },
662
+ });
663
+ }
664
+
665
+ /**
666
+ * @param {AST.Program} program
667
+ * @returns {AST.Program}
668
+ */
669
+ function expand_component_helpers(program) {
670
+ program.body = program.body.flatMap((statement) => {
671
+ if (statement.type === 'FunctionDeclaration') {
672
+ const helpers = /** @type {any} */ (statement.metadata)?.generated_helpers;
673
+ if (helpers?.length) {
674
+ return [...helpers, statement];
675
+ }
676
+ }
677
+
678
+ if (
679
+ (statement.type === 'ExportNamedDeclaration' ||
680
+ statement.type === 'ExportDefaultDeclaration') &&
681
+ statement.declaration?.type === 'FunctionDeclaration'
682
+ ) {
683
+ const helpers = /** @type {any} */ (statement.declaration.metadata)?.generated_helpers;
684
+ if (helpers?.length) {
685
+ return [...helpers, statement];
686
+ }
687
+ }
688
+
689
+ return [statement];
690
+ });
691
+
692
+ return program;
693
+ }
694
+
695
+ /**
696
+ * @param {any} node
697
+ * @returns {boolean}
698
+ */
699
+ function is_bare_return_statement(node) {
700
+ return node?.type === 'ReturnStatement' && node.argument == null;
701
+ }
702
+
703
+ /**
704
+ * @param {any} node
705
+ * @returns {boolean}
706
+ */
707
+ function is_lone_return_if_statement(node) {
708
+ if (node?.type !== 'IfStatement' || node.alternate) {
709
+ return false;
710
+ }
711
+
712
+ const consequent_body =
713
+ node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
714
+
715
+ return consequent_body.length === 1 && is_bare_return_statement(consequent_body[0]);
716
+ }
717
+
718
+ /**
719
+ * @param {any[]} render_nodes
720
+ * @param {any} source_node
721
+ * @returns {any}
722
+ */
723
+ function create_component_return_statement(render_nodes, source_node) {
724
+ return /** @type {any} */ ({
725
+ type: 'ReturnStatement',
726
+ argument: build_return_expression(render_nodes.slice()) || {
727
+ type: 'Literal',
728
+ value: null,
729
+ raw: 'null',
730
+ metadata: { path: [] },
731
+ },
732
+ metadata: { path: [] },
733
+ });
734
+ }
735
+
736
+ /**
737
+ * @param {any} node
738
+ * @param {any[]} render_nodes
739
+ * @returns {any}
740
+ */
741
+ function create_component_lone_return_if_statement(node, render_nodes) {
742
+ const consequent_body =
743
+ node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
744
+
745
+ return set_loc(
746
+ /** @type {any} */ ({
747
+ type: 'IfStatement',
748
+ test: node.test,
749
+ consequent: set_loc(
750
+ /** @type {any} */ ({
751
+ type: 'BlockStatement',
752
+ body: [create_component_return_statement(render_nodes, consequent_body[0])],
753
+ metadata: { path: [] },
754
+ }),
755
+ node.consequent,
756
+ ),
757
+ alternate: null,
758
+ metadata: { path: [] },
759
+ }),
760
+ node,
761
+ );
762
+ }
763
+
764
+ /**
765
+ * Mark every selector inside the stylesheet as "used" so `renderStylesheets`
766
+ * does not comment it out. We skip Ripple's selector-pruning pass because
767
+ * React component boundaries are dynamic — any selector authored inside the
768
+ * component's `<style>` block is considered intentional.
769
+ *
770
+ * @param {any} stylesheet
771
+ * @returns {any}
772
+ */
773
+ function prepare_stylesheet_for_render(stylesheet) {
774
+ walk(stylesheet, null, {
775
+ _(node, { next }) {
776
+ if (node && node.metadata && typeof node.metadata === 'object') {
777
+ node.metadata.used = true;
778
+ if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
779
+ node.metadata.scoped = true;
780
+ }
781
+ }
782
+ return next();
783
+ },
784
+ });
785
+ return stylesheet;
786
+ }
787
+
788
+ /**
789
+ * @param {any} node
790
+ * @returns {boolean}
791
+ */
792
+ function is_style_element(node) {
793
+ return (
794
+ node &&
795
+ node.type === 'Element' &&
796
+ node.id &&
797
+ node.id.type === 'Identifier' &&
798
+ node.id.name === 'style'
799
+ );
800
+ }
801
+
802
+ /**
803
+ * Recursively walk Element nodes within a component body and add the hash
804
+ * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
805
+ *
806
+ * @param {any} node
807
+ * @param {string} hash
808
+ * @returns {any}
809
+ */
810
+ function annotate_with_hash(node, hash) {
811
+ if (!node || typeof node !== 'object') return node;
812
+ if (
813
+ node.type === 'Component' ||
814
+ node.type === 'FunctionDeclaration' ||
815
+ node.type === 'FunctionExpression' ||
816
+ node.type === 'ArrowFunctionExpression'
817
+ ) {
818
+ return node;
819
+ }
820
+
821
+ if (node.type === 'Element') {
822
+ if (!is_style_element(node)) {
823
+ add_hash_class(node, hash);
824
+ }
825
+ if (Array.isArray(node.children)) {
826
+ node.children = node.children
827
+ .filter((/** @type {any} */ child) => !is_style_element(child))
828
+ .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
829
+ }
830
+ return node;
831
+ }
832
+
833
+ for (const key of Object.keys(node)) {
834
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
835
+ continue;
836
+ }
837
+
838
+ const value = node[key];
839
+ if (Array.isArray(value)) {
840
+ node[key] = value.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
841
+ } else if (value && typeof value === 'object') {
842
+ node[key] = annotate_with_hash(value, hash);
843
+ }
844
+ }
845
+
846
+ return node;
847
+ }
848
+
849
+ /**
850
+ * @param {any} component
851
+ * @param {string} hash
852
+ * @returns {void}
853
+ */
854
+ function annotate_component_with_hash(component, hash) {
855
+ /** @type {any[]} */
856
+ const body = component.body;
857
+ component.body = body
858
+ .filter((/** @type {any} */ child) => !is_style_element(child))
859
+ .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
860
+ }
861
+
862
+ /**
863
+ * Ensure the element carries a `class` attribute containing the scoping hash.
864
+ * @param {any} element
865
+ * @param {string} hash
866
+ */
867
+ function add_hash_class(element, hash) {
868
+ const attrs = element.attributes || (element.attributes = []);
869
+ const existing = attrs.find(
870
+ (/** @type {any} */ a) =>
871
+ a.type === 'Attribute' &&
872
+ a.name &&
873
+ a.name.type === 'Identifier' &&
874
+ (a.name.name === 'class' || a.name.name === 'className'),
875
+ );
876
+
877
+ if (!existing) {
878
+ attrs.push({
879
+ type: 'Attribute',
880
+ name: { type: 'Identifier', name: 'class' },
881
+ value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
882
+ });
883
+ return;
884
+ }
885
+
886
+ const value = existing.value;
887
+ if (!value) {
888
+ existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
889
+ return;
890
+ }
891
+
892
+ if (value.type === 'Literal' && typeof value.value === 'string') {
893
+ const merged = `${value.value} ${hash}`;
894
+ existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
895
+ return;
896
+ }
897
+
898
+ // Dynamic expression. Concatenate at runtime via template literal.
899
+ const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
900
+ existing.value = {
901
+ type: 'TemplateLiteral',
902
+ expressions: [expression],
903
+ quasis: [
904
+ {
905
+ type: 'TemplateElement',
906
+ value: { raw: '', cooked: '' },
907
+ tail: false,
908
+ },
909
+ {
910
+ type: 'TemplateElement',
911
+ value: { raw: ` ${hash}`, cooked: ` ${hash}` },
912
+ tail: true,
913
+ },
914
+ ],
915
+ };
916
+ }
917
+
918
+ /**
919
+ * @param {any} node
920
+ * @returns {boolean}
921
+ */
922
+ function is_jsx_child(node) {
923
+ if (!node) return false;
924
+ const t = node.type;
925
+ return (
926
+ t === 'JSXElement' ||
927
+ t === 'JSXFragment' ||
928
+ t === 'JSXExpressionContainer' ||
929
+ t === 'JSXText' ||
930
+ t === 'Tsx' ||
931
+ t === 'TsxCompat' ||
932
+ t === 'IfStatement' ||
933
+ t === 'ForOfStatement' ||
934
+ t === 'SwitchStatement' ||
935
+ t === 'TryStatement'
936
+ );
937
+ }
938
+
939
+ /**
940
+ * @param {any} node
941
+ * @param {TransformContext} transform_context
942
+ * @returns {any}
943
+ */
944
+ function to_jsx_element(node, transform_context) {
945
+ if (node.type === 'JSXElement') return node;
946
+ if (is_dynamic_element_id(node.id)) {
947
+ return dynamic_element_to_jsx_child(node, transform_context);
948
+ }
949
+
950
+ const name = identifier_to_jsx_name(node.id);
951
+ const attributes = (node.attributes || []).map(to_jsx_attribute);
952
+ const selfClosing = !!node.selfClosing;
953
+ const children = create_element_children(node.children || [], transform_context);
954
+ const has_unmappable_attribute = attributes.some(
955
+ (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
956
+ );
957
+
958
+ /** @type {ESTreeJSX.JSXOpeningElement} */
959
+ const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
960
+ has_unmappable_attribute
961
+ ? {
962
+ type: 'JSXOpeningElement',
963
+ name,
964
+ attributes,
965
+ selfClosing,
966
+ metadata: { path: [] },
967
+ }
968
+ : set_loc(
969
+ /** @type {any} */ ({
970
+ type: 'JSXOpeningElement',
971
+ name,
972
+ attributes,
973
+ selfClosing,
974
+ }),
975
+ node.openingElement || node,
976
+ )
977
+ );
978
+
979
+ /** @type {ESTreeJSX.JSXClosingElement | null} */
980
+ const closingElement = selfClosing
981
+ ? null
982
+ : set_loc(
983
+ /** @type {any} */ ({
984
+ type: 'JSXClosingElement',
985
+ name: clone_jsx_name(name, node.closingElement || node),
986
+ }),
987
+ node.closingElement || node,
988
+ );
989
+
990
+ return set_loc(
991
+ /** @type {any} */ ({
992
+ type: 'JSXElement',
993
+ openingElement,
994
+ closingElement,
995
+ children,
996
+ }),
997
+ node,
998
+ );
999
+ }
1000
+
1001
+ /**
1002
+ * @param {any[]} children
1003
+ * @param {TransformContext} transform_context
1004
+ * @returns {any[]}
1005
+ */
1006
+
1007
+ function create_element_children(children, transform_context) {
1008
+ if (children.length === 0) {
1009
+ return [];
1010
+ }
1011
+
1012
+ if (children.every(is_inline_element_child) && !children_contain_return_semantics(children)) {
1013
+ return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
1014
+ }
1015
+
1016
+ return [statement_body_to_jsx_child(children, transform_context)];
1017
+ }
1018
+
1019
+ /**
1020
+ * @param {any[]} children
1021
+ * @returns {boolean}
1022
+ */
1023
+ function children_contain_return_semantics(children) {
1024
+ return children.some(child_contains_return_semantics);
1025
+ }
1026
+
1027
+ /**
1028
+ * @param {any} node
1029
+ * @returns {boolean}
1030
+ */
1031
+ function child_contains_return_semantics(node) {
1032
+ if (!node || typeof node !== 'object') {
1033
+ return false;
1034
+ }
1035
+
1036
+ if (node.type === 'ReturnStatement' || is_lone_return_if_statement(node)) {
1037
+ return true;
1038
+ }
1039
+
1040
+ if (
1041
+ node.type === 'FunctionDeclaration' ||
1042
+ node.type === 'FunctionExpression' ||
1043
+ node.type === 'ArrowFunctionExpression' ||
1044
+ node.type === 'Component'
1045
+ ) {
1046
+ return false;
1047
+ }
1048
+
1049
+ if (Array.isArray(node)) {
1050
+ return node.some(child_contains_return_semantics);
1051
+ }
1052
+
1053
+ for (const key of Object.keys(node)) {
1054
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1055
+ continue;
1056
+ }
1057
+ if (child_contains_return_semantics(node[key])) {
1058
+ return true;
1059
+ }
1060
+ }
1061
+
1062
+ return false;
1063
+ }
1064
+
1065
+ /**
1066
+ * @param {any} node
1067
+ * @returns {boolean}
1068
+ */
1069
+ function is_inline_element_child(node) {
1070
+ return node && is_jsx_child(node);
1071
+ }
1072
+
1073
+ /**
1074
+ * @param {any[]} body_nodes
1075
+ * @param {TransformContext} transform_context
1076
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1077
+ */
1078
+ function statement_body_to_jsx_child(body_nodes, transform_context) {
1079
+ if (body_contains_top_level_hook_call(body_nodes)) {
1080
+ return hook_safe_statement_body_to_jsx_child(body_nodes, transform_context);
1081
+ }
1082
+
1083
+ return to_jsx_expression_container(
1084
+ /** @type {any} */ ({
1085
+ type: 'CallExpression',
1086
+ callee: {
1087
+ type: 'ArrowFunctionExpression',
1088
+ params: [],
1089
+ body: /** @type {any} */ ({
1090
+ type: 'BlockStatement',
1091
+ body: build_render_statements(body_nodes, true, transform_context),
1092
+ metadata: { path: [] },
1093
+ }),
1094
+ async: false,
1095
+ generator: false,
1096
+ expression: false,
1097
+ metadata: { path: [] },
1098
+ },
1099
+ arguments: [],
1100
+ optional: false,
1101
+ metadata: { path: [] },
1102
+ }),
1103
+ );
1104
+ }
1105
+
1106
+ /**
1107
+ * @param {any[]} body_nodes
1108
+ * @param {TransformContext} transform_context
1109
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1110
+ */
1111
+ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1112
+ const source_node = get_body_source_node(body_nodes);
1113
+ const helper_id = set_loc(
1114
+ create_generated_identifier(create_local_statement_component_name(transform_context)),
1115
+ source_node,
1116
+ );
1117
+ const helper_fn = set_loc(
1118
+ /** @type {any} */ ({
1119
+ type: 'FunctionDeclaration',
1120
+ id: helper_id,
1121
+ params: [],
1122
+ body: {
1123
+ type: 'BlockStatement',
1124
+ body: build_render_statements(body_nodes, true, transform_context),
1125
+ metadata: { path: [] },
1126
+ },
1127
+ async: false,
1128
+ generator: false,
1129
+ metadata: {
1130
+ path: [],
1131
+ is_component: true,
1132
+ is_method: false,
1133
+ },
1134
+ }),
1135
+ source_node,
1136
+ );
1137
+
1138
+ return to_jsx_expression_container(
1139
+ /** @type {any} */ ({
1140
+ type: 'CallExpression',
1141
+ callee: {
1142
+ type: 'ArrowFunctionExpression',
1143
+ params: [],
1144
+ body: /** @type {any} */ ({
1145
+ type: 'BlockStatement',
1146
+ body: [
1147
+ helper_fn,
1148
+ {
1149
+ type: 'ReturnStatement',
1150
+ argument: create_helper_component_element(helper_id, [], source_node),
1151
+ metadata: { path: [] },
1152
+ },
1153
+ ],
1154
+ metadata: { path: [] },
1155
+ }),
1156
+ async: false,
1157
+ generator: false,
1158
+ expression: false,
1159
+ metadata: { path: [] },
1160
+ },
1161
+ arguments: [],
1162
+ optional: false,
1163
+ metadata: { path: [] },
1164
+ }),
1165
+ source_node,
1166
+ );
1167
+ }
1168
+
1169
+ /**
1170
+ * @param {TransformContext} transform_context
1171
+ * @returns {string}
1172
+ */
1173
+ function create_local_statement_component_name(transform_context) {
1174
+ transform_context.local_statement_component_index += 1;
1175
+ return `StatementBodyHook${transform_context.local_statement_component_index}`;
1176
+ }
1177
+
1178
+ /**
1179
+ * Wraps a list of body nodes into a locally-declared component and returns
1180
+ * statements that declare the component then return `<ComponentName />`.
1181
+ * Used when a control flow branch contains hook calls that must be moved
1182
+ * into their own component boundary to satisfy the Rules of Hooks.
1183
+ *
1184
+ * @param {any[]} body_nodes
1185
+ * @param {any} key_expression - Optional key expression to add to the component element (for for-of loops)
1186
+ * @param {TransformContext} transform_context
1187
+ * @returns {any[]}
1188
+ */
1189
+ function hook_safe_render_statements(body_nodes, key_expression, transform_context) {
1190
+ const source_node = get_body_source_node(body_nodes);
1191
+ const helper_id = set_loc(
1192
+ create_generated_identifier(create_local_statement_component_name(transform_context)),
1193
+ source_node,
1194
+ );
1195
+
1196
+ const helper_fn = set_loc(
1197
+ /** @type {any} */ ({
1198
+ type: 'FunctionDeclaration',
1199
+ id: helper_id,
1200
+ params: [],
1201
+ body: {
1202
+ type: 'BlockStatement',
1203
+ body: build_render_statements(body_nodes, true, transform_context),
1204
+ metadata: { path: [] },
1205
+ },
1206
+ async: false,
1207
+ generator: false,
1208
+ metadata: {
1209
+ path: [],
1210
+ is_component: true,
1211
+ is_method: false,
1212
+ },
1213
+ }),
1214
+ source_node,
1215
+ );
1216
+
1217
+ const component_element = create_helper_component_element(helper_id, [], source_node);
1218
+
1219
+ if (key_expression) {
1220
+ component_element.openingElement.attributes.push(
1221
+ /** @type {any} */ ({
1222
+ type: 'JSXAttribute',
1223
+ name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
1224
+ value: to_jsx_expression_container(key_expression, key_expression),
1225
+ metadata: { path: [] },
1226
+ }),
1227
+ );
1228
+ }
1229
+
1230
+ return [
1231
+ helper_fn,
1232
+ {
1233
+ type: 'ReturnStatement',
1234
+ argument: component_element,
1235
+ metadata: { path: [] },
1236
+ },
1237
+ ];
1238
+ }
1239
+
1240
+ /**
1241
+ * @param {any[]} body_nodes
1242
+ * @returns {any}
1243
+ */
1244
+ function get_body_source_node(body_nodes) {
1245
+ const first = body_nodes[0];
1246
+ const last = body_nodes[body_nodes.length - 1];
1247
+
1248
+ if (first?.loc && last?.loc) {
1249
+ return {
1250
+ start: first.start,
1251
+ end: last.end,
1252
+ loc: {
1253
+ start: first.loc.start,
1254
+ end: last.loc.end,
1255
+ },
1256
+ };
1257
+ }
1258
+
1259
+ return first;
1260
+ }
1261
+
1262
+ /**
1263
+ * @param {any} node
1264
+ * @param {TransformContext} transform_context
1265
+ * @returns {any}
1266
+ */
1267
+ function to_jsx_child(node, transform_context) {
1268
+ if (!node) return node;
1269
+ switch (node.type) {
1270
+ case 'Tsx':
1271
+ return tsx_node_to_jsx_expression(node);
1272
+ case 'TsxCompat':
1273
+ return tsx_compat_node_to_jsx_expression(node);
1274
+ case 'Element':
1275
+ return to_jsx_element(node, transform_context);
1276
+ case 'Text':
1277
+ case 'TSRXExpression':
1278
+ return to_jsx_expression_container(node.expression, node);
1279
+ case 'IfStatement':
1280
+ return if_statement_to_jsx_child(node, transform_context);
1281
+ case 'ForOfStatement':
1282
+ return for_of_statement_to_jsx_child(node, transform_context);
1283
+ case 'SwitchStatement':
1284
+ return switch_statement_to_jsx_child(node, transform_context);
1285
+ case 'TryStatement':
1286
+ return try_statement_to_jsx_child(node, transform_context);
1287
+ default:
1288
+ return node;
1289
+ }
1290
+ }
1291
+
1292
+ /**
1293
+ * @param {any} node
1294
+ * @param {TransformContext} transform_context
1295
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1296
+ */
1297
+ function if_statement_to_jsx_child(node, transform_context) {
1298
+ return to_jsx_expression_container(
1299
+ /** @type {any} */ ({
1300
+ type: 'CallExpression',
1301
+ callee: {
1302
+ type: 'ArrowFunctionExpression',
1303
+ params: [],
1304
+ body: /** @type {any} */ ({
1305
+ type: 'BlockStatement',
1306
+ body: [
1307
+ create_render_if_statement(node, transform_context),
1308
+ create_null_return_statement(),
1309
+ ],
1310
+ metadata: { path: [] },
1311
+ }),
1312
+ async: false,
1313
+ generator: false,
1314
+ expression: false,
1315
+ metadata: { path: [] },
1316
+ },
1317
+ arguments: [],
1318
+ optional: false,
1319
+ metadata: { path: [] },
1320
+ }),
1321
+ );
1322
+ }
1323
+
1324
+ /**
1325
+ * Find the first `key` attribute expression in the top-level elements of a body.
1326
+ * Used to propagate keys from loop body elements to wrapper components.
1327
+ * Works on both pre-transform (Ripple Element) and post-transform (JSXElement) nodes.
1328
+ *
1329
+ * @param {any[]} body_nodes
1330
+ * @returns {any | undefined}
1331
+ */
1332
+ function find_key_expression_in_body(body_nodes) {
1333
+ for (const node of body_nodes) {
1334
+ // Pre-transform: Ripple Element node
1335
+ if (node.type === 'Element') {
1336
+ for (const attr of node.attributes || []) {
1337
+ if (attr.type === 'Attribute') {
1338
+ const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
1339
+ if (attr_name === 'key') {
1340
+ return attr.value?.expression ?? attr.value;
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ // Post-transform: JSXElement node
1346
+ if (node.type === 'JSXElement') {
1347
+ for (const attr of node.openingElement?.attributes || []) {
1348
+ if (
1349
+ attr.type === 'JSXAttribute' &&
1350
+ attr.name?.type === 'JSXIdentifier' &&
1351
+ attr.name.name === 'key'
1352
+ ) {
1353
+ // Value is a JSXExpressionContainer
1354
+ if (attr.value?.type === 'JSXExpressionContainer') {
1355
+ return attr.value.expression;
1356
+ }
1357
+ return attr.value;
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ return undefined;
1363
+ }
1364
+
1365
+ /**
1366
+ * @param {any} node
1367
+ * @param {TransformContext} transform_context
1368
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1369
+ */
1370
+ function for_of_statement_to_jsx_child(node, transform_context) {
1371
+ if (node.key) {
1372
+ throw create_compile_error(
1373
+ node.key,
1374
+ 'React TSRX does not support `key` in `for` control flow. Put the key on the rendered element instead, for example `<div key={i}>...</div>`.',
1375
+ );
1376
+ }
1377
+
1378
+ const loop_params = get_for_of_iteration_params(node.left, node.index);
1379
+ const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
1380
+ const has_hooks = body_contains_top_level_hook_call(loop_body);
1381
+ const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1382
+
1383
+ return to_jsx_expression_container(
1384
+ /** @type {any} */ ({
1385
+ type: 'CallExpression',
1386
+ callee: {
1387
+ type: 'MemberExpression',
1388
+ object: node.right,
1389
+ property: create_generated_identifier('map'),
1390
+ computed: false,
1391
+ optional: false,
1392
+ metadata: { path: [] },
1393
+ },
1394
+ arguments: [
1395
+ {
1396
+ type: 'ArrowFunctionExpression',
1397
+ params: loop_params,
1398
+ body: /** @type {any} */ ({
1399
+ type: 'BlockStatement',
1400
+ body: has_hooks
1401
+ ? hook_safe_render_statements(loop_body, key_expression, transform_context)
1402
+ : build_render_statements(loop_body, true, transform_context),
1403
+ metadata: { path: [] },
1404
+ }),
1405
+ async: false,
1406
+ generator: false,
1407
+ expression: false,
1408
+ metadata: { path: [] },
1409
+ },
1410
+ ],
1411
+ async: false,
1412
+ optional: false,
1413
+ metadata: { path: [] },
1414
+ }),
1415
+ );
1416
+ }
1417
+
1418
+ /**
1419
+ * @param {any} node
1420
+ * @param {TransformContext} transform_context
1421
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1422
+ */
1423
+ function switch_statement_to_jsx_child(node, transform_context) {
1424
+ return to_jsx_expression_container(
1425
+ /** @type {any} */ ({
1426
+ type: 'CallExpression',
1427
+ callee: {
1428
+ type: 'ArrowFunctionExpression',
1429
+ params: [],
1430
+ body: /** @type {any} */ ({
1431
+ type: 'BlockStatement',
1432
+ body: [
1433
+ create_render_switch_statement(node, transform_context),
1434
+ create_null_return_statement(),
1435
+ ],
1436
+ metadata: { path: [] },
1437
+ }),
1438
+ async: false,
1439
+ generator: false,
1440
+ expression: false,
1441
+ metadata: { path: [] },
1442
+ },
1443
+ arguments: [],
1444
+ optional: false,
1445
+ metadata: { path: [] },
1446
+ }),
1447
+ );
1448
+ }
1449
+
1450
+ /**
1451
+ * Transform a `try { ... } pending { ... } catch (err, reset) { ... }` block
1452
+ * into React `<TsrxErrorBoundary>` and/or `<Suspense>` JSX elements.
1453
+ *
1454
+ * - `pending` → `<Suspense fallback={...}>`
1455
+ * - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
1456
+ * - both → ErrorBoundary wraps Suspense
1457
+ * - `finally` blocks are not supported in component template context
1458
+ *
1459
+ * @param {any} node
1460
+ * @param {TransformContext} transform_context
1461
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1462
+ */
1463
+ function try_statement_to_jsx_child(node, transform_context) {
1464
+ const pending = node.pending;
1465
+ const handler = node.handler;
1466
+ const finalizer = node.finalizer;
1467
+
1468
+ if (finalizer) {
1469
+ throw create_compile_error(
1470
+ finalizer,
1471
+ 'React TSRX does not support `finally` blocks in component templates. Move the try statement into a function if you need a finally block.',
1472
+ );
1473
+ }
1474
+
1475
+ if (!pending && !handler) {
1476
+ throw create_compile_error(
1477
+ node,
1478
+ 'Component try statements must have a `pending` or `catch` block.',
1479
+ );
1480
+ }
1481
+
1482
+ // Validate that try body contains JSX if pending block is present
1483
+ if (pending) {
1484
+ const try_body = node.block.body || [];
1485
+ if (!try_body.some(is_jsx_child)) {
1486
+ throw create_compile_error(
1487
+ node.block,
1488
+ 'Component try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
1489
+ );
1490
+ }
1491
+ const pending_body = pending.body || [];
1492
+ if (!pending_body.some(is_jsx_child)) {
1493
+ throw create_compile_error(
1494
+ pending,
1495
+ 'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
1496
+ );
1497
+ }
1498
+ }
1499
+
1500
+ // Build the try body content as JSX children
1501
+ const try_body_nodes = node.block.body || [];
1502
+ const try_content = statement_body_to_jsx_child(try_body_nodes, transform_context);
1503
+
1504
+ /** @type {any} */
1505
+ let result = try_content;
1506
+
1507
+ // Wrap in <Suspense> if pending block exists
1508
+ if (pending) {
1509
+ transform_context.needs_suspense = true;
1510
+ const pending_body_nodes = pending.body || [];
1511
+ const fallback_content = statement_body_to_jsx_child(pending_body_nodes, transform_context);
1512
+
1513
+ result = create_jsx_element(
1514
+ 'Suspense',
1515
+ [
1516
+ {
1517
+ type: 'JSXAttribute',
1518
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
1519
+ value: fallback_content,
1520
+ metadata: { path: [] },
1521
+ },
1522
+ ],
1523
+ [result],
1524
+ );
1525
+ }
1526
+
1527
+ // Wrap in <TsrxErrorBoundary> if catch block exists
1528
+ if (handler) {
1529
+ transform_context.needs_error_boundary = true;
1530
+
1531
+ const catch_params = [];
1532
+ if (handler.param) {
1533
+ catch_params.push(handler.param);
1534
+ } else {
1535
+ catch_params.push(create_generated_identifier('_error'));
1536
+ }
1537
+ if (handler.resetParam) {
1538
+ catch_params.push(handler.resetParam);
1539
+ } else {
1540
+ catch_params.push(create_generated_identifier('_reset'));
1541
+ }
1542
+
1543
+ const catch_body_nodes = handler.body.body || [];
1544
+ const fallback_fn = {
1545
+ type: 'ArrowFunctionExpression',
1546
+ params: catch_params,
1547
+ body: /** @type {any} */ ({
1548
+ type: 'BlockStatement',
1549
+ body: build_render_statements(catch_body_nodes, true, transform_context),
1550
+ metadata: { path: [] },
1551
+ }),
1552
+ async: false,
1553
+ generator: false,
1554
+ expression: false,
1555
+ metadata: { path: [] },
1556
+ };
1557
+
1558
+ result = create_jsx_element(
1559
+ 'TsrxErrorBoundary',
1560
+ [
1561
+ {
1562
+ type: 'JSXAttribute',
1563
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
1564
+ value: to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
1565
+ metadata: { path: [] },
1566
+ },
1567
+ ],
1568
+ [result],
1569
+ );
1570
+ }
1571
+
1572
+ // result is a JSXElement, but we need to return a JSXExpressionContainer
1573
+ // for embedding in the parent component's render return
1574
+ if (result.type === 'JSXElement') {
1575
+ return to_jsx_expression_container(result);
1576
+ }
1577
+
1578
+ return result;
1579
+ }
1580
+
1581
+ /**
1582
+ * Create a simple JSX element AST node.
1583
+ *
1584
+ * @param {string} tag_name
1585
+ * @param {any[]} attributes
1586
+ * @param {any[]} children
1587
+ * @returns {any}
1588
+ */
1589
+ function create_jsx_element(tag_name, attributes, children) {
1590
+ const name = { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } };
1591
+ return {
1592
+ type: 'JSXElement',
1593
+ openingElement: {
1594
+ type: 'JSXOpeningElement',
1595
+ name,
1596
+ attributes,
1597
+ selfClosing: children.length === 0,
1598
+ metadata: { path: [] },
1599
+ },
1600
+ closingElement:
1601
+ children.length > 0
1602
+ ? {
1603
+ type: 'JSXClosingElement',
1604
+ name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
1605
+ metadata: { path: [] },
1606
+ }
1607
+ : null,
1608
+ children,
1609
+ metadata: { path: [] },
1610
+ };
1611
+ }
1612
+
1613
+ /**
1614
+ * Inject import declarations for `Suspense` and `TsrxErrorBoundary` if the
1615
+ * transform determined they are needed.
1616
+ *
1617
+ * @param {AST.Program} program
1618
+ * @param {TransformContext} transform_context
1619
+ */
1620
+ function inject_try_imports(program, transform_context) {
1621
+ /** @type {any[]} */
1622
+ const imports = [];
1623
+
1624
+ if (transform_context.needs_suspense) {
1625
+ imports.push({
1626
+ type: 'ImportDeclaration',
1627
+ specifiers: [
1628
+ {
1629
+ type: 'ImportSpecifier',
1630
+ imported: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
1631
+ local: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
1632
+ metadata: { path: [] },
1633
+ },
1634
+ ],
1635
+ source: { type: 'Literal', value: 'react', raw: "'react'" },
1636
+ metadata: { path: [] },
1637
+ });
1638
+ }
1639
+
1640
+ if (transform_context.needs_error_boundary) {
1641
+ imports.push({
1642
+ type: 'ImportDeclaration',
1643
+ specifiers: [
1644
+ {
1645
+ type: 'ImportSpecifier',
1646
+ imported: {
1647
+ type: 'Identifier',
1648
+ name: 'TsrxErrorBoundary',
1649
+ metadata: { path: [] },
1650
+ },
1651
+ local: {
1652
+ type: 'Identifier',
1653
+ name: 'TsrxErrorBoundary',
1654
+ metadata: { path: [] },
1655
+ },
1656
+ metadata: { path: [] },
1657
+ },
1658
+ ],
1659
+ source: {
1660
+ type: 'Literal',
1661
+ value: '@tsrx/react/error-boundary',
1662
+ raw: "'@tsrx/react/error-boundary'",
1663
+ },
1664
+ metadata: { path: [] },
1665
+ });
1666
+ }
1667
+
1668
+ if (imports.length > 0) {
1669
+ program.body.unshift(...imports);
1670
+ }
1671
+ }
1672
+
1673
+ /**
1674
+ * @param {any} node
1675
+ * @param {TransformContext} transform_context
1676
+ * @returns {any}
1677
+ */
1678
+ function create_render_if_statement(node, transform_context) {
1679
+ const consequent_body =
1680
+ node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
1681
+ const consequent_has_hooks = body_contains_top_level_hook_call(consequent_body);
1682
+
1683
+ let alternate = null;
1684
+ if (node.alternate) {
1685
+ if (node.alternate.type === 'IfStatement') {
1686
+ alternate = create_render_if_statement(node.alternate, transform_context);
1687
+ } else {
1688
+ const alternate_body = node.alternate.body || [node.alternate];
1689
+ const alternate_has_hooks = body_contains_top_level_hook_call(alternate_body);
1690
+ alternate = set_loc(
1691
+ /** @type {any} */ ({
1692
+ type: 'BlockStatement',
1693
+ body: alternate_has_hooks
1694
+ ? hook_safe_render_statements(alternate_body, undefined, transform_context)
1695
+ : build_render_statements(alternate_body, true, transform_context),
1696
+ metadata: { path: [] },
1697
+ }),
1698
+ node.alternate,
1699
+ );
1700
+ }
1701
+ }
1702
+
1703
+ return set_loc(
1704
+ {
1705
+ type: 'IfStatement',
1706
+ test: node.test,
1707
+ consequent: set_loc(
1708
+ /** @type {any} */ ({
1709
+ type: 'BlockStatement',
1710
+ body: consequent_has_hooks
1711
+ ? hook_safe_render_statements(consequent_body, undefined, transform_context)
1712
+ : build_render_statements(consequent_body, true, transform_context),
1713
+ metadata: { path: [] },
1714
+ }),
1715
+ node.consequent,
1716
+ ),
1717
+ alternate,
1718
+ },
1719
+ node,
1720
+ );
1721
+ }
1722
+
1723
+ /**
1724
+ * @param {any} node
1725
+ * @param {TransformContext} transform_context
1726
+ * @returns {any}
1727
+ */
1728
+ function create_render_switch_statement(node, transform_context) {
1729
+ return /** @type {any} */ ({
1730
+ type: 'SwitchStatement',
1731
+ discriminant: node.discriminant,
1732
+ cases: node.cases.map((/** @type {any} */ c) =>
1733
+ create_render_switch_case(c, transform_context),
1734
+ ),
1735
+ metadata: { path: [] },
1736
+ });
1737
+ }
1738
+
1739
+ /**
1740
+ * @param {any} switch_case
1741
+ * @param {TransformContext} transform_context
1742
+ * @returns {any}
1743
+ */
1744
+ function create_render_switch_case(switch_case, transform_context) {
1745
+ const consequent = flatten_switch_consequent(switch_case.consequent || []);
1746
+
1747
+ // Strip trailing break statements for hook analysis
1748
+ const body_without_break = [];
1749
+ for (const child of consequent) {
1750
+ if (child.type === 'BreakStatement') break;
1751
+ body_without_break.push(child);
1752
+ }
1753
+
1754
+ if (body_contains_top_level_hook_call(body_without_break)) {
1755
+ return /** @type {any} */ ({
1756
+ type: 'SwitchCase',
1757
+ test: switch_case.test,
1758
+ consequent: hook_safe_render_statements(body_without_break, undefined, transform_context),
1759
+ metadata: { path: [] },
1760
+ });
1761
+ }
1762
+
1763
+ const case_body = [];
1764
+ const render_nodes = [];
1765
+ let has_terminal = false;
1766
+
1767
+ for (const child of consequent) {
1768
+ if (child.type === 'BreakStatement') {
1769
+ if (render_nodes.length > 0 && !has_terminal) {
1770
+ case_body.push(create_component_return_statement(render_nodes, switch_case));
1771
+ } else if (!has_terminal) {
1772
+ case_body.push(child);
1773
+ }
1774
+ has_terminal = true;
1775
+ break;
1776
+ }
1777
+
1778
+ if (is_bare_return_statement(child)) {
1779
+ case_body.push(create_component_return_statement(render_nodes, child));
1780
+ has_terminal = true;
1781
+ break;
1782
+ }
1783
+
1784
+ if (is_jsx_child(child)) {
1785
+ render_nodes.push(to_jsx_child(child, transform_context));
1786
+ } else {
1787
+ case_body.push(child);
1788
+ }
1789
+ }
1790
+
1791
+ if (!has_terminal && render_nodes.length > 0) {
1792
+ case_body.push(create_component_return_statement(render_nodes, switch_case));
1793
+ }
1794
+
1795
+ return /** @type {any} */ ({
1796
+ type: 'SwitchCase',
1797
+ test: switch_case.test,
1798
+ consequent: case_body,
1799
+ metadata: { path: [] },
1800
+ });
1801
+ }
1802
+
1803
+ /**
1804
+ * @returns {any}
1805
+ */
1806
+ function create_null_return_statement() {
1807
+ return {
1808
+ type: 'ReturnStatement',
1809
+ argument: { type: 'Literal', value: null, raw: 'null' },
1810
+ };
1811
+ }
1812
+
1813
+ /**
1814
+ * @param {AST.Expression} expression
1815
+ * @param {any} [source_node]
1816
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1817
+ */
1818
+ function to_jsx_expression_container(expression, source_node = expression) {
1819
+ // NOTE: JSXExpressionContainer nodes are intentionally created without loc.
1820
+ // They are synthetic wrappers whose source positions do not correspond to
1821
+ // entries in the generated source map, so adding loc causes Volar mapping failures.
1822
+ return /** @type {any} */ ({
1823
+ type: 'JSXExpressionContainer',
1824
+ expression: /** @type {any} */ (expression),
1825
+ metadata: { path: [] },
1826
+ });
1827
+ }
1828
+
1829
+ /**
1830
+ * @param {any} attr
1831
+ * @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
1832
+ */
1833
+ function to_jsx_attribute(attr) {
1834
+ if (!attr) return attr;
1835
+ if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') {
1836
+ return attr;
1837
+ }
1838
+ if (attr.type === 'SpreadAttribute') {
1839
+ return set_loc(
1840
+ /** @type {any} */ ({
1841
+ type: 'JSXSpreadAttribute',
1842
+ argument: attr.argument,
1843
+ }),
1844
+ attr,
1845
+ );
1846
+ }
1847
+ if (attr.type === 'RefAttribute') {
1848
+ // RefAttribute uses `{ref expr}` syntax whose source positions don't map to the
1849
+ // generated `ref={expr}` JSX attribute, so we intentionally omit loc.
1850
+ return /** @type {any} */ ({
1851
+ type: 'JSXAttribute',
1852
+ name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
1853
+ value: to_jsx_expression_container(attr.argument),
1854
+ shorthand: false,
1855
+ metadata: { path: [] },
1856
+ });
1857
+ }
1858
+
1859
+ // Rewrite Ripple-style `class` → React's `className`.
1860
+ let attr_name = attr.name;
1861
+ if (attr_name && attr_name.type === 'Identifier' && attr_name.name === 'class') {
1862
+ attr_name = set_loc(
1863
+ /** @type {any} */ ({ type: 'Identifier', name: 'className', metadata: { path: [] } }),
1864
+ attr.name,
1865
+ );
1866
+ }
1867
+
1868
+ const name =
1869
+ attr_name && attr_name.type === 'Identifier' ? identifier_to_jsx_name(attr_name) : attr_name;
1870
+
1871
+ let value = attr.value;
1872
+ if (value) {
1873
+ if (value.type === 'Literal' && typeof value.value === 'string') {
1874
+ // Keep string literal as attribute string.
1875
+ } else if (value.type !== 'JSXExpressionContainer') {
1876
+ value = to_jsx_expression_container(value);
1877
+ }
1878
+ }
1879
+
1880
+ const jsx_attribute = /** @type {any} */ ({
1881
+ type: 'JSXAttribute',
1882
+ name,
1883
+ value: value || null,
1884
+ shorthand: false,
1885
+ metadata: { path: [] },
1886
+ });
1887
+
1888
+ if (value_has_unmappable_jsx_loc(value)) {
1889
+ /** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
1890
+ return jsx_attribute;
1891
+ }
1892
+
1893
+ return set_loc(jsx_attribute, attr);
1894
+ }
1895
+
1896
+ /**
1897
+ * @param {any} value
1898
+ * @returns {boolean}
1899
+ */
1900
+ function value_has_unmappable_jsx_loc(value) {
1901
+ return !!(
1902
+ value?.type === 'JSXExpressionContainer' &&
1903
+ (value.expression?.type === 'JSXElement' || value.expression?.type === 'JSXFragment') &&
1904
+ !value.expression.loc
1905
+ );
1906
+ }
1907
+
1908
+ /**
1909
+ * @param {any} id
1910
+ * @returns {boolean}
1911
+ */
1912
+ function is_dynamic_element_id(id) {
1913
+ if (!id || typeof id !== 'object') {
1914
+ return false;
1915
+ }
1916
+
1917
+ if (id.type === 'Identifier') {
1918
+ return !!id.tracked;
1919
+ }
1920
+
1921
+ if (id.type === 'MemberExpression') {
1922
+ return is_dynamic_element_id(id.object);
1923
+ }
1924
+
1925
+ return false;
1926
+ }
1927
+
1928
+ /**
1929
+ * @param {any} node
1930
+ * @param {TransformContext} transform_context
1931
+ * @returns {ESTreeJSX.JSXExpressionContainer}
1932
+ */
1933
+ function dynamic_element_to_jsx_child(node, transform_context) {
1934
+ const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
1935
+ const alias_declaration = set_loc(
1936
+ /** @type {any} */ ({
1937
+ type: 'VariableDeclaration',
1938
+ kind: 'const',
1939
+ declarations: [
1940
+ {
1941
+ type: 'VariableDeclarator',
1942
+ id: dynamic_id,
1943
+ init: clone_expression_node(node.id),
1944
+ metadata: { path: [] },
1945
+ },
1946
+ ],
1947
+ metadata: { path: [] },
1948
+ }),
1949
+ node,
1950
+ );
1951
+ const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
1952
+
1953
+ return to_jsx_expression_container(
1954
+ /** @type {any} */ ({
1955
+ type: 'CallExpression',
1956
+ callee: {
1957
+ type: 'ArrowFunctionExpression',
1958
+ params: [],
1959
+ body: /** @type {any} */ ({
1960
+ type: 'BlockStatement',
1961
+ body: [
1962
+ alias_declaration,
1963
+ {
1964
+ type: 'ReturnStatement',
1965
+ argument: {
1966
+ type: 'ConditionalExpression',
1967
+ test: clone_identifier(dynamic_id),
1968
+ consequent: jsx_element,
1969
+ alternate: create_null_literal(),
1970
+ metadata: { path: [] },
1971
+ },
1972
+ metadata: { path: [] },
1973
+ },
1974
+ ],
1975
+ metadata: { path: [] },
1976
+ }),
1977
+ async: false,
1978
+ generator: false,
1979
+ expression: false,
1980
+ metadata: { path: [] },
1981
+ },
1982
+ arguments: [],
1983
+ optional: false,
1984
+ metadata: { path: [] },
1985
+ }),
1986
+ node,
1987
+ );
1988
+ }
1989
+
1990
+ /**
1991
+ * @param {AST.Identifier} dynamic_id
1992
+ * @param {any} node
1993
+ * @param {TransformContext} transform_context
1994
+ * @returns {ESTreeJSX.JSXElement}
1995
+ */
1996
+ function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
1997
+ const attributes = (node.attributes || []).map(to_jsx_attribute);
1998
+ const selfClosing = !!node.selfClosing;
1999
+ const children = create_element_children(node.children || [], transform_context);
2000
+ const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
2001
+
2002
+ return /** @type {any} */ ({
2003
+ type: 'JSXElement',
2004
+ openingElement: {
2005
+ type: 'JSXOpeningElement',
2006
+ name,
2007
+ attributes,
2008
+ selfClosing,
2009
+ metadata: { path: [] },
2010
+ },
2011
+ closingElement: selfClosing
2012
+ ? null
2013
+ : {
2014
+ type: 'JSXClosingElement',
2015
+ name: clone_jsx_name(name),
2016
+ metadata: { path: [] },
2017
+ },
2018
+ children,
2019
+ metadata: { path: [] },
2020
+ });
2021
+ }
2022
+
2023
+ /**
2024
+ * @param {any} node
2025
+ * @returns {any}
2026
+ */
2027
+ function clone_expression_node(node) {
2028
+ if (!node || typeof node !== 'object') {
2029
+ return node;
2030
+ }
2031
+
2032
+ if (Array.isArray(node)) {
2033
+ return node.map(clone_expression_node);
2034
+ }
2035
+
2036
+ const clone = { ...node };
2037
+ for (const key of Object.keys(clone)) {
2038
+ if (key === 'metadata') {
2039
+ clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
2040
+ continue;
2041
+ }
2042
+ clone[key] = clone_expression_node(clone[key]);
2043
+ }
2044
+ return clone;
2045
+ }
2046
+
2047
+ /**
2048
+ * @param {AST.Identifier | AST.MemberExpression | any} id
2049
+ * @returns {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression}
2050
+ */
2051
+ function identifier_to_jsx_name(id) {
2052
+ if (id.type === 'Identifier') {
2053
+ return set_loc(
2054
+ /** @type {any} */ ({
2055
+ type: 'JSXIdentifier',
2056
+ name: id.name,
2057
+ metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
2058
+ }),
2059
+ id,
2060
+ );
2061
+ }
2062
+ if (id.type === 'MemberExpression') {
2063
+ return set_loc(
2064
+ /** @type {any} */ ({
2065
+ type: 'JSXMemberExpression',
2066
+ object: /** @type {any} */ (identifier_to_jsx_name(id.object)),
2067
+ property: /** @type {any} */ (identifier_to_jsx_name(id.property)),
2068
+ }),
2069
+ id,
2070
+ );
2071
+ }
2072
+ return id;
2073
+ }
2074
+
2075
+ /**
2076
+ * @param {any} name
2077
+ * @param {any} [source_node]
2078
+ * @returns {any}
2079
+ */
2080
+ function clone_jsx_name(name, source_node = name) {
2081
+ if (name.type === 'JSXIdentifier') {
2082
+ return set_loc(
2083
+ {
2084
+ type: 'JSXIdentifier',
2085
+ name: name.name,
2086
+ metadata: name.metadata || { path: [] },
2087
+ },
2088
+ source_node,
2089
+ );
2090
+ }
2091
+ if (name.type === 'JSXMemberExpression') {
2092
+ return set_loc(
2093
+ {
2094
+ type: 'JSXMemberExpression',
2095
+ object: clone_jsx_name(name.object, source_node.object || name.object),
2096
+ property: clone_jsx_name(name.property, source_node.property || name.property),
2097
+ metadata: name.metadata || { path: [] },
2098
+ },
2099
+ source_node,
2100
+ );
2101
+ }
2102
+ return name;
2103
+ }
2104
+
2105
+ /**
2106
+ * @param {any[]} render_nodes
2107
+ * @returns {any}
2108
+ */
2109
+ function build_return_expression(render_nodes) {
2110
+ if (render_nodes.length === 0) return null;
2111
+ if (render_nodes.length === 1) {
2112
+ const only = render_nodes[0];
2113
+ if (only.type === 'JSXExpressionContainer') {
2114
+ return only.expression;
2115
+ }
2116
+ return only;
2117
+ }
2118
+ const first = render_nodes[0];
2119
+ const last = render_nodes[render_nodes.length - 1];
2120
+ return set_loc(
2121
+ {
2122
+ type: 'JSXFragment',
2123
+ openingFragment: /** @type {any} */ ({
2124
+ type: 'JSXOpeningFragment',
2125
+ metadata: { path: [] },
2126
+ }),
2127
+ closingFragment: /** @type {any} */ ({
2128
+ type: 'JSXClosingFragment',
2129
+ metadata: { path: [] },
2130
+ }),
2131
+ children: render_nodes,
2132
+ metadata: { path: [] },
2133
+ },
2134
+ first?.loc && last?.loc
2135
+ ? {
2136
+ start: first.start,
2137
+ end: last.end,
2138
+ loc: {
2139
+ start: first.loc.start,
2140
+ end: last.loc.end,
2141
+ },
2142
+ }
2143
+ : undefined,
2144
+ );
2145
+ }
2146
+
2147
+ /**
2148
+ * @template T
2149
+ * @param {T} node
2150
+ * @param {any} source_node
2151
+ * @returns {T}
2152
+ */
2153
+ function set_loc(node, source_node) {
2154
+ /** @type {any} */ (node).metadata ??= { path: [] };
2155
+ if (source_node?.loc) {
2156
+ return /** @type {T} */ (setLocation(/** @type {any} */ (node), source_node, true));
2157
+ }
2158
+ return node;
2159
+ }
2160
+
2161
+ /**
2162
+ * @param {any} left
2163
+ * @param {any} index
2164
+ * @returns {AST.Pattern[]}
2165
+ */
2166
+ function get_for_of_iteration_params(left, index) {
2167
+ const params = [];
2168
+ if (left?.type === 'VariableDeclaration') {
2169
+ params.push(left.declarations[0]?.id);
2170
+ } else {
2171
+ params.push(left);
2172
+ }
2173
+ if (index) {
2174
+ params.push(index);
2175
+ }
2176
+ return params;
2177
+ }
2178
+
2179
+ /**
2180
+ * @param {string} name
2181
+ * @returns {AST.Identifier}
2182
+ */
2183
+ function create_generated_identifier(name) {
2184
+ return /** @type {any} */ ({
2185
+ type: 'Identifier',
2186
+ name,
2187
+ metadata: { path: [] },
2188
+ });
2189
+ }
2190
+
2191
+ /**
2192
+ * @param {any} node
2193
+ * @param {string} message
2194
+ * @returns {Error & { pos: number, end: number }}
2195
+ */
2196
+ function create_compile_error(node, message) {
2197
+ const error = /** @type {Error & { pos: number, end: number }} */ (new Error(message));
2198
+ error.pos = node.start ?? 0;
2199
+ error.end = node.end ?? error.pos + 1;
2200
+ return error;
2201
+ }
2202
+
2203
+ /**
2204
+ * @param {any} node
2205
+ * @returns {any}
2206
+ */
2207
+ function tsx_compat_node_to_jsx_expression(node) {
2208
+ if (node.kind !== 'react') {
2209
+ throw create_compile_error(
2210
+ node,
2211
+ `React TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or <tsx:react>.`,
2212
+ );
2213
+ }
2214
+
2215
+ return tsx_node_to_jsx_expression(node);
2216
+ }
2217
+
2218
+ /**
2219
+ * @param {any} node
2220
+ * @returns {any}
2221
+ */
2222
+ function tsx_node_to_jsx_expression(node) {
2223
+ const children = (node.children || []).filter(
2224
+ (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
2225
+ );
2226
+
2227
+ if (children.length === 1 && children[0].type !== 'JSXText') {
2228
+ return strip_locations(children[0]);
2229
+ }
2230
+
2231
+ return strip_locations(
2232
+ /** @type {any} */ ({
2233
+ type: 'JSXFragment',
2234
+ openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
2235
+ closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
2236
+ children,
2237
+ metadata: { path: [] },
2238
+ }),
2239
+ );
2240
+ }
2241
+
2242
+ /**
2243
+ * @param {any} node
2244
+ * @returns {any}
2245
+ */
2246
+ function strip_locations(node) {
2247
+ if (!node || typeof node !== 'object') {
2248
+ return node;
2249
+ }
2250
+
2251
+ if (Array.isArray(node)) {
2252
+ return node.map(strip_locations);
2253
+ }
2254
+
2255
+ delete node.loc;
2256
+ delete node.start;
2257
+ delete node.end;
2258
+
2259
+ for (const key of Object.keys(node)) {
2260
+ if (key === 'metadata') {
2261
+ continue;
2262
+ }
2263
+ node[key] = strip_locations(node[key]);
2264
+ }
2265
+
2266
+ return node;
2267
+ }
2268
+
2269
+ /**
2270
+ * @param {any[]} consequent
2271
+ * @returns {any[]}
2272
+ */
2273
+ function flatten_switch_consequent(consequent) {
2274
+ const result = [];
2275
+ for (const node of consequent) {
2276
+ if (node.type === 'BlockStatement') {
2277
+ result.push(...node.body);
2278
+ } else {
2279
+ result.push(node);
2280
+ }
2281
+ }
2282
+ return result;
2283
+ }