@tsrx/solid 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,1687 @@
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 {
8
+ renderStylesheets,
9
+ setLocation,
10
+ applyLazyTransforms as apply_lazy_transforms,
11
+ collectLazyBindingsFromComponent as collect_lazy_bindings_from_component,
12
+ preallocateLazyIds as preallocate_lazy_ids,
13
+ replaceLazyParams as replace_lazy_params,
14
+ prepareStylesheetForRender as prepare_stylesheet_for_render,
15
+ annotateComponentWithHash as annotate_component_with_hash,
16
+ } from '@tsrx/core';
17
+
18
+ /**
19
+ * @typedef {{
20
+ * needs_show: boolean,
21
+ * needs_for: boolean,
22
+ * needs_switch: boolean,
23
+ * needs_match: boolean,
24
+ * needs_errored: boolean,
25
+ * needs_loading: boolean,
26
+ * lazy_next_id: number,
27
+ * current_css_hash: string | null,
28
+ * }} TransformContext
29
+ */
30
+
31
+ /**
32
+ * @typedef {{ source_name: string, read: () => any }} LazyBinding
33
+ */
34
+
35
+ /**
36
+ * Transform a parsed tsrx-solid AST into a TSX module targeting Solid 2.0.
37
+ *
38
+ * Each `component` declaration becomes a plain `FunctionDeclaration` that
39
+ * returns Solid JSX. Control flow statements are rewritten to Solid's
40
+ * built-in components (`<Show>`, `<Switch>/<Match>`, `<For>`, `<Errored>`,
41
+ * `<Loading>`) so they remain reactive. Per-component `<style>` blocks are
42
+ * collected, rendered via `@tsrx/core`'s stylesheet renderer, and returned
43
+ * alongside the JS output so a downstream plugin can inject them.
44
+ *
45
+ * @param {AST.Program} ast
46
+ * @param {string} source
47
+ * @param {string} [filename]
48
+ * @returns {{ ast: AST.Program, code: string, map: any, css: { code: string, hash: string } | null }}
49
+ */
50
+ export function transform(ast, source, filename) {
51
+ /** @type {any[]} */
52
+ const stylesheets = [];
53
+
54
+ /** @type {TransformContext} */
55
+ const transform_context = {
56
+ needs_show: false,
57
+ needs_for: false,
58
+ needs_switch: false,
59
+ needs_match: false,
60
+ needs_errored: false,
61
+ needs_loading: false,
62
+ lazy_next_id: 0,
63
+ current_css_hash: null,
64
+ };
65
+
66
+ preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
67
+
68
+ // First pass: collect stylesheets and annotate elements with the component hash.
69
+ walk(/** @type {any} */ (ast), transform_context, {
70
+ Component(node, { next, state }) {
71
+ const as_any = /** @type {any} */ (node);
72
+ const css = as_any.css;
73
+ if (css) {
74
+ stylesheets.push(css);
75
+ annotate_component_with_hash(as_any, css.hash);
76
+ }
77
+ return next(state);
78
+ },
79
+ });
80
+
81
+ // Second pass: transform Components, Elements, Text nodes, Tsx blocks, etc.
82
+ const transformed = walk(/** @type {any} */ (ast), transform_context, {
83
+ Component(node, { next, state }) {
84
+ const as_any = /** @type {any} */ (node);
85
+
86
+ const saved_css_hash = state.current_css_hash;
87
+ state.current_css_hash = as_any.css ? as_any.css.hash : null;
88
+
89
+ const inner = /** @type {any} */ (next() ?? node);
90
+
91
+ state.current_css_hash = saved_css_hash;
92
+
93
+ return /** @type {any} */ (component_to_function_declaration(inner, state));
94
+ },
95
+
96
+ Tsx(node, { next }) {
97
+ const inner = /** @type {any} */ (next() ?? node);
98
+ return /** @type {any} */ (tsx_node_to_jsx_expression(inner));
99
+ },
100
+
101
+ TsxCompat(node, { next }) {
102
+ const inner = /** @type {any} */ (next() ?? node);
103
+ return /** @type {any} */ (tsx_compat_node_to_jsx_expression(inner));
104
+ },
105
+
106
+ Element(node, { next, state }) {
107
+ const inner = /** @type {any} */ (next() ?? node);
108
+ return /** @type {any} */ (to_jsx_element(inner, state));
109
+ },
110
+
111
+ // `Text` nodes are lowered by `to_jsx_child` (and the `textContent`
112
+ // optimization in `to_jsx_element`) rather than the walker, so the
113
+ // parent element still sees a raw `Text` child when it runs and can
114
+ // decide whether to hoist it up to an attribute.
115
+ TSRXExpression(node, { next }) {
116
+ const inner = /** @type {any} */ (next() ?? node);
117
+ return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
118
+ },
119
+
120
+ MemberExpression(node, { next, state }) {
121
+ const as_any = /** @type {any} */ (node);
122
+ if (as_any.object && as_any.object.type === 'StyleIdentifier' && state.current_css_hash) {
123
+ const class_name = as_any.computed ? as_any.property.value : as_any.property.name;
124
+ const value = `${state.current_css_hash} ${class_name}`;
125
+ return /** @type {any} */ ({ type: 'Literal', value, raw: JSON.stringify(value) });
126
+ }
127
+ return next();
128
+ },
129
+ });
130
+
131
+ inject_solid_imports(/** @type {AST.Program} */ (transformed), transform_context);
132
+
133
+ // Apply lazy destructuring transforms to module-level code (top-level function
134
+ // declarations, arrow functions, etc.). Component bodies have already been
135
+ // transformed inside component_to_function_declaration; this catches plain
136
+ // functions outside components and any lazy patterns in module scope.
137
+ const final_program = /** @type {any} */ (
138
+ apply_lazy_transforms(/** @type {any} */ (transformed), new Map())
139
+ );
140
+
141
+ const result = print(/** @type {any} */ (final_program), tsx(), {
142
+ sourceMapSource: filename,
143
+ sourceMapContent: source,
144
+ });
145
+
146
+ const css =
147
+ stylesheets.length > 0
148
+ ? {
149
+ code: renderStylesheets(
150
+ /** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
151
+ ),
152
+ hash: stylesheets.map((s) => s.hash).join(' '),
153
+ }
154
+ : null;
155
+
156
+ return {
157
+ ast: /** @type {AST.Program} */ (final_program),
158
+ code: result.code,
159
+ map: result.map,
160
+ css,
161
+ };
162
+ }
163
+
164
+ // =====================================================================
165
+ // Component → FunctionDeclaration
166
+ // =====================================================================
167
+
168
+ /**
169
+ * @param {any} component
170
+ * @param {TransformContext} transform_context
171
+ * @returns {AST.FunctionDeclaration}
172
+ */
173
+ function component_to_function_declaration(component, transform_context) {
174
+ const params = component.params || [];
175
+ const body = /** @type {any[]} */ (component.body || []);
176
+
177
+ const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
178
+
179
+ // Detect top-level early-return pattern: `if (cond) { return; }`.
180
+ // Solid components run their body once at setup, so an early `return` would
181
+ // make subsequent statements and JSX permanently inert. To preserve
182
+ // React-like "stop rendering the rest when cond becomes true" semantics,
183
+ // lift JSX from after the early `if` (plus any JSX that appears before
184
+ // it, since that too must disappear when cond flips) into a
185
+ // `<Show when={!cond}>` whose function-children re-runs when cond changes.
186
+ // Non-JSX statements on either side stay in the outer body so setup code
187
+ // (signal creation, resource declarations, etc.) runs exactly once at
188
+ // component setup — putting them inside the `<Show>` arrow would re-run
189
+ // them on every toggle, creating fresh signals and losing state.
190
+ //
191
+ // The `if` node itself is elided: its `test` expression lives on in the
192
+ // `<Show when={!cond}>` attribute and is evaluated reactively by Solid's
193
+ // runtime, so any side effects or reactive reads in `cond` are preserved.
194
+ // Non-JSX statements after the guard run unconditionally rather than being
195
+ // gated by it; this is an intentional divergence from imperative `return`
196
+ // semantics required by the setup-once component model.
197
+ const early_idx = body.findIndex(is_early_return_if);
198
+ /** @type {any[]} */
199
+ let effective_body = body;
200
+ if (early_idx !== -1) {
201
+ const early_if = /** @type {any} */ (body[early_idx]);
202
+ const before = body.slice(0, early_idx);
203
+ const after = body.slice(early_idx + 1);
204
+ /** @type {any[]} */
205
+ const before_non_jsx = [];
206
+ /** @type {any[]} */
207
+ const before_jsx = [];
208
+ for (const child of before) {
209
+ if (is_jsx_child(child)) before_jsx.push(child);
210
+ else before_non_jsx.push(child);
211
+ }
212
+ /** @type {any[]} */
213
+ const after_non_jsx = [];
214
+ /** @type {any[]} */
215
+ const after_jsx = [];
216
+ for (const child of after) {
217
+ if (is_jsx_child(child)) after_jsx.push(child);
218
+ else after_non_jsx.push(child);
219
+ }
220
+ const lifted = [...before_jsx, ...after_jsx];
221
+ if (lifted.length > 0) {
222
+ transform_context.needs_show = true;
223
+ const show_body = body_to_jsx_child(lifted, transform_context);
224
+ const show_element = build_show_element(negate_expression(early_if.test), show_body, null);
225
+ effective_body = [...before_non_jsx, ...after_non_jsx, show_element];
226
+ }
227
+ }
228
+
229
+ const statements = [];
230
+ const render_nodes = [];
231
+
232
+ for (const child of effective_body) {
233
+ if (is_jsx_child(child)) {
234
+ render_nodes.push(to_jsx_child(child, transform_context));
235
+ } else {
236
+ statements.push(child);
237
+ }
238
+ }
239
+
240
+ if (render_nodes.length > 0) {
241
+ statements.push(
242
+ /** @type {any} */ ({
243
+ type: 'ReturnStatement',
244
+ argument: build_return_expression(render_nodes) || {
245
+ type: 'Literal',
246
+ value: null,
247
+ raw: 'null',
248
+ metadata: { path: [] },
249
+ },
250
+ metadata: { path: [] },
251
+ }),
252
+ );
253
+ }
254
+
255
+ const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
256
+
257
+ const body_block = /** @type {any} */ ({
258
+ type: 'BlockStatement',
259
+ body: statements,
260
+ metadata: { path: [] },
261
+ });
262
+ const final_body =
263
+ lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
264
+
265
+ const fn = /** @type {any} */ ({
266
+ type: 'FunctionDeclaration',
267
+ id: component.id,
268
+ params: final_params,
269
+ body: final_body,
270
+ async: false,
271
+ generator: false,
272
+ metadata: {
273
+ path: [],
274
+ is_component: true,
275
+ },
276
+ });
277
+
278
+ if (fn.id) {
279
+ fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
280
+ ...fn.id.metadata,
281
+ is_component: true,
282
+ });
283
+ }
284
+
285
+ setLocation(fn, /** @type {any} */ (component), true);
286
+ return fn;
287
+ }
288
+
289
+ // =====================================================================
290
+ // Control flow → Solid JSX components
291
+ // =====================================================================
292
+
293
+ /**
294
+ * @param {any} node
295
+ * @returns {boolean}
296
+ */
297
+ function is_jsx_child(node) {
298
+ if (!node) return false;
299
+ const t = node.type;
300
+ return (
301
+ t === 'JSXElement' ||
302
+ t === 'JSXFragment' ||
303
+ t === 'JSXExpressionContainer' ||
304
+ t === 'JSXText' ||
305
+ t === 'Tsx' ||
306
+ t === 'TsxCompat' ||
307
+ t === 'Element' ||
308
+ t === 'Text' ||
309
+ t === 'TSRXExpression' ||
310
+ t === 'Html' ||
311
+ t === 'IfStatement' ||
312
+ t === 'ForOfStatement' ||
313
+ t === 'SwitchStatement' ||
314
+ t === 'TryStatement'
315
+ );
316
+ }
317
+
318
+ /**
319
+ * @param {any} node
320
+ * @param {TransformContext} transform_context
321
+ * @returns {any}
322
+ */
323
+ function to_jsx_child(node, transform_context) {
324
+ if (!node) return node;
325
+ switch (node.type) {
326
+ case 'Tsx':
327
+ return tsx_node_to_jsx_expression(node);
328
+ case 'TsxCompat':
329
+ return tsx_compat_node_to_jsx_expression(node);
330
+ case 'Element':
331
+ return to_jsx_element(node, transform_context);
332
+ case 'Text':
333
+ return to_jsx_expression_container(to_text_expression(node.expression, node), node);
334
+ case 'TSRXExpression':
335
+ return to_jsx_expression_container(node.expression, node);
336
+ case 'Html':
337
+ throw new Error(
338
+ '`{html ...}` is not supported on the Solid target. Use `innerHTML={...}` as an element attribute instead.',
339
+ );
340
+ case 'IfStatement':
341
+ return if_statement_to_jsx_child(node, transform_context);
342
+ case 'ForOfStatement':
343
+ return for_of_statement_to_jsx_child(node, transform_context);
344
+ case 'SwitchStatement':
345
+ return switch_statement_to_jsx_child(node, transform_context);
346
+ case 'TryStatement':
347
+ return try_statement_to_jsx_child(node, transform_context);
348
+ default:
349
+ return node;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Convert a list of body nodes to a Solid JSX child.
355
+ *
356
+ * If the body is purely JSX, returns the JSX node (or fragment) directly.
357
+ *
358
+ * If the body contains non-JSX statements (declarations, throws, etc.), we
359
+ * must preserve them — they may declare signals, throw errors, or perform
360
+ * other branch-local setup that subsequent JSX depends on. We wrap them in
361
+ * an `ArrowFunctionExpression` whose block body is
362
+ * `() => { ...statements; return <>...jsx</>; }`
363
+ * Callers are responsible for placing that arrow where Solid's runtime will
364
+ * actually call it:
365
+ * - `<Show>` / `<Match>` children: invoked as function children via
366
+ * {@link to_function_child} which ensures `length > 0` so Solid's
367
+ * runtime calls them with a condition accessor.
368
+ * - `<For>` / `<Errored fallback>`: the outer iteration/fallback arrow's
369
+ * body is merged with the branch arrow's body via
370
+ * {@link merge_branch_body_into_arrow}.
371
+ * - Fallback props (`<Show fallback>`, `<Switch fallback>`,
372
+ * `<Loading fallback>`): IIFE-wrapped via {@link iife_if_arrow}.
373
+ *
374
+ * @param {any[]} body_nodes
375
+ * @param {TransformContext} transform_context
376
+ * @returns {any}
377
+ */
378
+ function body_to_jsx_child(body_nodes, transform_context) {
379
+ /** @type {any[]} */
380
+ const statements = [];
381
+ /** @type {any[]} */
382
+ const children = [];
383
+ for (const child of body_nodes) {
384
+ if (is_jsx_child(child)) {
385
+ children.push(to_jsx_child(child, transform_context));
386
+ } else {
387
+ statements.push(child);
388
+ }
389
+ }
390
+
391
+ if (statements.length === 0) {
392
+ if (children.length === 0) return create_null_literal();
393
+ if (children.length === 1) {
394
+ const only = children[0];
395
+ if (only.type === 'JSXExpressionContainer') return only.expression;
396
+ return only;
397
+ }
398
+ return build_return_expression(children);
399
+ }
400
+
401
+ // Branch body has non-JSX statements: wrap everything in an arrow so the
402
+ // statements run when (and only when) the branch actually renders.
403
+ /** @type {any[]} */
404
+ const block_body = [...statements];
405
+ if (children.length > 0) {
406
+ block_body.push(
407
+ /** @type {any} */ ({
408
+ type: 'ReturnStatement',
409
+ argument: build_return_expression(children),
410
+ metadata: { path: [] },
411
+ }),
412
+ );
413
+ }
414
+
415
+ return /** @type {any} */ ({
416
+ type: 'ArrowFunctionExpression',
417
+ params: [],
418
+ body: {
419
+ type: 'BlockStatement',
420
+ body: block_body,
421
+ metadata: { path: [] },
422
+ },
423
+ async: false,
424
+ generator: false,
425
+ expression: false,
426
+ metadata: { path: [], is_branch_arrow: true },
427
+ });
428
+ }
429
+
430
+ /**
431
+ * @param {any} node
432
+ * @returns {boolean}
433
+ */
434
+ function is_branch_arrow(node) {
435
+ return (
436
+ node &&
437
+ node.type === 'ArrowFunctionExpression' &&
438
+ node.metadata &&
439
+ node.metadata.is_branch_arrow === true
440
+ );
441
+ }
442
+
443
+ /**
444
+ * Turn a branch arrow (`() => { ...; return jsx; }`) into a function child
445
+ * that Solid's `<Show>` / `<Match>` runtime will actually invoke. Those
446
+ * components only call `children` as a function when `children.length > 0`,
447
+ * so we give the arrow a single underscore-prefixed parameter that it
448
+ * ignores.
449
+ *
450
+ * If the input isn't a branch arrow, it's returned unchanged.
451
+ *
452
+ * @param {any} node
453
+ * @returns {any}
454
+ */
455
+ function to_function_child(node) {
456
+ if (!is_branch_arrow(node)) return node;
457
+ return {
458
+ ...node,
459
+ params: [create_generated_identifier('_')],
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Inline a branch arrow's statements into an existing arrow (e.g. the
465
+ * `(item, i) => ...` passed to `<For>` or the `(err, reset) => ...` passed
466
+ * to `<Errored fallback>`). Returns the arrow with its body replaced by the
467
+ * merged block.
468
+ *
469
+ * @param {any} outer_arrow
470
+ * @param {any} branch_body
471
+ * @returns {any}
472
+ */
473
+ function merge_branch_body_into_arrow(outer_arrow, branch_body) {
474
+ if (!is_branch_arrow(branch_body)) {
475
+ return { ...outer_arrow, body: branch_body, expression: true };
476
+ }
477
+ return {
478
+ ...outer_arrow,
479
+ body: branch_body.body,
480
+ expression: false,
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Detect the top-level early-return pattern `if (cond) { return; }` (or
486
+ * `if (cond) return;`) with no `else` branch.
487
+ *
488
+ * @param {any} node
489
+ * @returns {boolean}
490
+ */
491
+ function is_early_return_if(node) {
492
+ if (!node || node.type !== 'IfStatement' || node.alternate) return false;
493
+ const consequent = node.consequent;
494
+ if (!consequent) return false;
495
+ if (consequent.type === 'ReturnStatement' && !consequent.argument) return true;
496
+ if (
497
+ consequent.type === 'BlockStatement' &&
498
+ consequent.body.length === 1 &&
499
+ consequent.body[0].type === 'ReturnStatement' &&
500
+ !consequent.body[0].argument
501
+ ) {
502
+ return true;
503
+ }
504
+ return false;
505
+ }
506
+
507
+ /**
508
+ * Build a logical-negation (`!expr`) expression.
509
+ *
510
+ * @param {any} expr
511
+ * @returns {any}
512
+ */
513
+ function negate_expression(expr) {
514
+ return {
515
+ type: 'UnaryExpression',
516
+ operator: '!',
517
+ prefix: true,
518
+ argument: expr,
519
+ metadata: { path: [] },
520
+ };
521
+ }
522
+
523
+ /**
524
+ * Wrap a branch arrow in an IIFE so it can be used as a prop value (e.g.
525
+ * `<Show fallback={...}>`). Returns non-arrow inputs unchanged.
526
+ *
527
+ * @param {any} node
528
+ * @returns {any}
529
+ */
530
+ function iife_if_arrow(node) {
531
+ if (!is_branch_arrow(node)) return node;
532
+ return {
533
+ type: 'CallExpression',
534
+ callee: node,
535
+ arguments: [],
536
+ optional: false,
537
+ metadata: { path: [] },
538
+ };
539
+ }
540
+
541
+ /**
542
+ * `if (test) { ... }` → `<Show when={test}>...</Show>`
543
+ * `if (test) { a } else { b }` → `<Show when={test} fallback={b}>a</Show>`
544
+ * `if (a) { } else if (b) { } else { }` → `<Switch fallback={...}><Match when={a}>...</Match>...</Switch>`
545
+ *
546
+ * @param {any} node
547
+ * @param {TransformContext} transform_context
548
+ * @returns {any}
549
+ */
550
+ function if_statement_to_jsx_child(node, transform_context) {
551
+ const branches = flatten_if_chain(node);
552
+
553
+ if (branches.length === 1) {
554
+ // Single `if` with no else → <Show when>
555
+ transform_context.needs_show = true;
556
+ const [{ test, body }] = branches;
557
+ return build_show_element(test, body_to_jsx_child(body, transform_context), null);
558
+ }
559
+
560
+ if (branches.length === 2 && branches[1].test === null) {
561
+ // Plain if/else → <Show when fallback>
562
+ transform_context.needs_show = true;
563
+ const [if_branch, else_branch] = branches;
564
+ return build_show_element(
565
+ if_branch.test,
566
+ body_to_jsx_child(if_branch.body, transform_context),
567
+ body_to_jsx_child(else_branch.body, transform_context),
568
+ );
569
+ }
570
+
571
+ // 3+ branches → <Switch fallback>{<Match when>...</Match>}...</Switch>
572
+ transform_context.needs_switch = true;
573
+ transform_context.needs_match = true;
574
+
575
+ let fallback = null;
576
+ const match_branches = [];
577
+ for (const branch of branches) {
578
+ if (branch.test === null) {
579
+ fallback = body_to_jsx_child(branch.body, transform_context);
580
+ } else {
581
+ match_branches.push(branch);
582
+ }
583
+ }
584
+
585
+ const attributes =
586
+ fallback !== null
587
+ ? [
588
+ {
589
+ type: 'JSXAttribute',
590
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
591
+ value: to_jsx_expression_container(iife_if_arrow(fallback)),
592
+ metadata: { path: [] },
593
+ },
594
+ ]
595
+ : [];
596
+
597
+ const children = match_branches.map((branch) =>
598
+ create_jsx_element(
599
+ 'Match',
600
+ [
601
+ {
602
+ type: 'JSXAttribute',
603
+ name: { type: 'JSXIdentifier', name: 'when', metadata: { path: [] } },
604
+ value: to_jsx_expression_container(branch.test),
605
+ metadata: { path: [] },
606
+ },
607
+ ],
608
+ [jsx_child_wrap(to_function_child(body_to_jsx_child(branch.body, transform_context)))],
609
+ ),
610
+ );
611
+
612
+ return create_jsx_element('Switch', attributes, children);
613
+ }
614
+
615
+ /**
616
+ * Flatten an if/else-if chain into an array of `{ test, body }` branches.
617
+ * The final `else` (if present) is represented as `{ test: null, body }`.
618
+ *
619
+ * @param {any} node
620
+ * @returns {{ test: any, body: any[] }[]}
621
+ */
622
+ function flatten_if_chain(node) {
623
+ const branches = [];
624
+ /** @type {any} */
625
+ let current = node;
626
+ while (current && current.type === 'IfStatement') {
627
+ const consequent_body =
628
+ current.consequent.type === 'BlockStatement' ? current.consequent.body : [current.consequent];
629
+ branches.push({ test: current.test, body: consequent_body });
630
+ if (current.alternate && current.alternate.type === 'IfStatement') {
631
+ current = current.alternate;
632
+ continue;
633
+ }
634
+ if (current.alternate) {
635
+ const alt_body =
636
+ current.alternate.type === 'BlockStatement' ? current.alternate.body : [current.alternate];
637
+ branches.push({ test: null, body: alt_body });
638
+ }
639
+ break;
640
+ }
641
+ return branches;
642
+ }
643
+
644
+ /**
645
+ * @param {any} test
646
+ * @param {any} children
647
+ * @param {any} fallback
648
+ * @returns {any}
649
+ */
650
+ function build_show_element(test, children, fallback) {
651
+ const attributes = [
652
+ {
653
+ type: 'JSXAttribute',
654
+ name: { type: 'JSXIdentifier', name: 'when', metadata: { path: [] } },
655
+ value: to_jsx_expression_container(test),
656
+ metadata: { path: [] },
657
+ },
658
+ ];
659
+ if (fallback !== null && fallback !== undefined) {
660
+ attributes.push({
661
+ type: 'JSXAttribute',
662
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
663
+ value: to_jsx_expression_container(iife_if_arrow(fallback)),
664
+ metadata: { path: [] },
665
+ });
666
+ }
667
+ return create_jsx_element('Show', attributes, [jsx_child_wrap(to_function_child(children))]);
668
+ }
669
+
670
+ /**
671
+ * `for (const item of items; index i) { ... }` →
672
+ * `<For each={items}>{(item, i) => ...}</For>`
673
+ *
674
+ * `for (const item of items; key item.id) { ... }` →
675
+ * `<For each={items} keyed={(item) => item.id}>{(item) => ...}</For>`
676
+ *
677
+ * Solid 2.0's `<For>` accepts a `keyed` prop (`boolean | (item) => any`) that
678
+ * switches reconciliation from reference identity to derived keys. The callback
679
+ * only receives the item — not the index — so a `key` expression that depends
680
+ * only on the index can't be translated cleanly and will surface as a
681
+ * scope error in the generated TSX. Item-based keys (the common case, e.g.
682
+ * `key item.id`) translate directly.
683
+ *
684
+ * @param {any} node
685
+ * @param {TransformContext} transform_context
686
+ * @returns {any}
687
+ */
688
+ function for_of_statement_to_jsx_child(node, transform_context) {
689
+ transform_context.needs_for = true;
690
+
691
+ const loop_params = get_for_of_iteration_params(node.left, node.index);
692
+ const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
693
+
694
+ const body_jsx = body_to_jsx_child(loop_body, transform_context);
695
+
696
+ const arrow = merge_branch_body_into_arrow(
697
+ /** @type {any} */ ({
698
+ type: 'ArrowFunctionExpression',
699
+ params: loop_params,
700
+ body: null,
701
+ async: false,
702
+ generator: false,
703
+ expression: true,
704
+ metadata: { path: [] },
705
+ }),
706
+ body_jsx,
707
+ );
708
+
709
+ const attributes = [
710
+ {
711
+ type: 'JSXAttribute',
712
+ name: { type: 'JSXIdentifier', name: 'each', metadata: { path: [] } },
713
+ value: to_jsx_expression_container(node.right),
714
+ metadata: { path: [] },
715
+ },
716
+ ];
717
+
718
+ if (node.key) {
719
+ const item_param = clone_expression_node(loop_params[0]);
720
+ const keyed_arrow = /** @type {any} */ ({
721
+ type: 'ArrowFunctionExpression',
722
+ params: [item_param],
723
+ body: node.key,
724
+ async: false,
725
+ generator: false,
726
+ expression: true,
727
+ metadata: { path: [] },
728
+ });
729
+ attributes.push(
730
+ /** @type {any} */ ({
731
+ type: 'JSXAttribute',
732
+ name: { type: 'JSXIdentifier', name: 'keyed', metadata: { path: [] } },
733
+ value: to_jsx_expression_container(keyed_arrow, node.key),
734
+ metadata: { path: [] },
735
+ }),
736
+ );
737
+ }
738
+
739
+ return create_jsx_element('For', attributes, [to_jsx_expression_container(arrow)]);
740
+ }
741
+
742
+ /**
743
+ * Solid doesn't have a dedicated `<Switch>` statement — we reuse the
744
+ * `<Switch>/<Match>` components pair that `if` chains use. A `switch`
745
+ * statement with a discriminant `d` and cases `[c1, c2, default]` becomes:
746
+ * <Switch fallback={...default}><Match when={d === c1}>...</Match>...</Switch>
747
+ *
748
+ * @param {any} node
749
+ * @param {TransformContext} transform_context
750
+ * @returns {any}
751
+ */
752
+ function switch_statement_to_jsx_child(node, transform_context) {
753
+ transform_context.needs_switch = true;
754
+ transform_context.needs_match = true;
755
+
756
+ /** @type {any} */
757
+ let fallback = null;
758
+ const match_children = [];
759
+
760
+ for (const switch_case of node.cases) {
761
+ const consequent = flatten_switch_consequent(switch_case.consequent || []);
762
+ const body = [];
763
+ for (const child of consequent) {
764
+ if (child.type === 'BreakStatement') break;
765
+ body.push(child);
766
+ }
767
+
768
+ const body_jsx = body_to_jsx_child(body, transform_context);
769
+ if (switch_case.test === null) {
770
+ fallback = body_jsx;
771
+ continue;
772
+ }
773
+
774
+ // Clone the discriminant per-case: every generated `<Match when={d === caseN}>`
775
+ // would otherwise share the same AST node reference, so a downstream pass
776
+ // (lazy transforms, printer metadata, source-map annotation) mutating it on
777
+ // one case would corrupt the others.
778
+ const test = /** @type {any} */ ({
779
+ type: 'BinaryExpression',
780
+ operator: '===',
781
+ left: clone_expression_node(node.discriminant),
782
+ right: switch_case.test,
783
+ metadata: { path: [] },
784
+ });
785
+
786
+ match_children.push(
787
+ create_jsx_element(
788
+ 'Match',
789
+ [
790
+ {
791
+ type: 'JSXAttribute',
792
+ name: { type: 'JSXIdentifier', name: 'when', metadata: { path: [] } },
793
+ value: to_jsx_expression_container(test),
794
+ metadata: { path: [] },
795
+ },
796
+ ],
797
+ [jsx_child_wrap(to_function_child(body_jsx))],
798
+ ),
799
+ );
800
+ }
801
+
802
+ const attributes =
803
+ fallback !== null
804
+ ? [
805
+ {
806
+ type: 'JSXAttribute',
807
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
808
+ value: to_jsx_expression_container(iife_if_arrow(fallback)),
809
+ metadata: { path: [] },
810
+ },
811
+ ]
812
+ : [];
813
+
814
+ return create_jsx_element('Switch', attributes, match_children);
815
+ }
816
+
817
+ /**
818
+ * Transform a `try { ... } pending { ... } catch (err, reset) { ... }` block
819
+ * into Solid's `<Errored>` and/or `<Loading>` JSX elements.
820
+ *
821
+ * @param {any} node
822
+ * @param {TransformContext} transform_context
823
+ * @returns {any}
824
+ */
825
+ function try_statement_to_jsx_child(node, transform_context) {
826
+ const pending = node.pending;
827
+ const handler = node.handler;
828
+ const finalizer = node.finalizer;
829
+
830
+ if (finalizer) {
831
+ throw create_compile_error(
832
+ finalizer,
833
+ 'Solid TSRX does not support `finally` blocks in component templates. Move the try statement into a function if you need a finally block.',
834
+ );
835
+ }
836
+
837
+ if (!pending && !handler) {
838
+ throw create_compile_error(
839
+ node,
840
+ 'Component try statements must have a `pending` or `catch` block.',
841
+ );
842
+ }
843
+
844
+ const try_body_nodes = node.block.body || [];
845
+ /** @type {any} */
846
+ let result = jsx_child_wrap(iife_if_arrow(body_to_jsx_child(try_body_nodes, transform_context)));
847
+
848
+ if (pending) {
849
+ transform_context.needs_loading = true;
850
+ const pending_body_nodes = pending.body || [];
851
+ const fallback_content = body_to_jsx_child(pending_body_nodes, transform_context);
852
+
853
+ result = create_jsx_element(
854
+ 'Loading',
855
+ [
856
+ {
857
+ type: 'JSXAttribute',
858
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
859
+ value: to_jsx_expression_container(iife_if_arrow(fallback_content)),
860
+ metadata: { path: [] },
861
+ },
862
+ ],
863
+ [result],
864
+ );
865
+ }
866
+
867
+ if (handler) {
868
+ transform_context.needs_errored = true;
869
+
870
+ const catch_params = [];
871
+ if (handler.param) catch_params.push(handler.param);
872
+ else catch_params.push(create_generated_identifier('_error'));
873
+ if (handler.resetParam) catch_params.push(handler.resetParam);
874
+ else catch_params.push(create_generated_identifier('_reset'));
875
+
876
+ const catch_body_nodes = handler.body.body || [];
877
+ const catch_jsx = body_to_jsx_child(catch_body_nodes, transform_context);
878
+
879
+ const fallback_fn = merge_branch_body_into_arrow(
880
+ /** @type {any} */ ({
881
+ type: 'ArrowFunctionExpression',
882
+ params: catch_params,
883
+ body: null,
884
+ async: false,
885
+ generator: false,
886
+ expression: true,
887
+ metadata: { path: [] },
888
+ }),
889
+ catch_jsx,
890
+ );
891
+
892
+ result = create_jsx_element(
893
+ 'Errored',
894
+ [
895
+ {
896
+ type: 'JSXAttribute',
897
+ name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
898
+ value: to_jsx_expression_container(fallback_fn),
899
+ metadata: { path: [] },
900
+ },
901
+ ],
902
+ [result],
903
+ );
904
+ }
905
+
906
+ return result;
907
+ }
908
+
909
+ /**
910
+ * If `child` is already a JSX child node return it; otherwise wrap in
911
+ * a JSXExpressionContainer so it can live inside a JSX element's children list.
912
+ *
913
+ * @param {any} child
914
+ * @returns {any}
915
+ */
916
+ function jsx_child_wrap(child) {
917
+ if (!child) return child;
918
+ if (child.type === 'JSXElement' || child.type === 'JSXFragment') return child;
919
+ return to_jsx_expression_container(child);
920
+ }
921
+
922
+ /**
923
+ * @param {string} tag_name
924
+ * @param {any[]} attributes
925
+ * @param {any[]} children
926
+ * @returns {any}
927
+ */
928
+ function create_jsx_element(tag_name, attributes, children) {
929
+ const name = { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } };
930
+ const filtered_children = children.filter(Boolean);
931
+ return {
932
+ type: 'JSXElement',
933
+ openingElement: {
934
+ type: 'JSXOpeningElement',
935
+ name,
936
+ attributes,
937
+ selfClosing: filtered_children.length === 0,
938
+ metadata: { path: [] },
939
+ },
940
+ closingElement:
941
+ filtered_children.length > 0
942
+ ? {
943
+ type: 'JSXClosingElement',
944
+ name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
945
+ metadata: { path: [] },
946
+ }
947
+ : null,
948
+ children: filtered_children,
949
+ metadata: { path: [] },
950
+ };
951
+ }
952
+
953
+ /**
954
+ * Inject `import { Show, For, Switch, Match, Errored, Loading } from 'solid-js'`
955
+ * specifiers for whichever control-flow primitives the transform emitted.
956
+ *
957
+ * @param {AST.Program} program
958
+ * @param {TransformContext} transform_context
959
+ */
960
+ function inject_solid_imports(program, transform_context) {
961
+ const needed = [];
962
+ if (transform_context.needs_show) needed.push('Show');
963
+ if (transform_context.needs_for) needed.push('For');
964
+ if (transform_context.needs_switch) needed.push('Switch');
965
+ if (transform_context.needs_match) needed.push('Match');
966
+ if (transform_context.needs_errored) needed.push('Errored');
967
+ if (transform_context.needs_loading) needed.push('Loading');
968
+
969
+ if (needed.length === 0) return;
970
+
971
+ const specifiers = needed.map((name) => ({
972
+ type: 'ImportSpecifier',
973
+ imported: { type: 'Identifier', name, metadata: { path: [] } },
974
+ local: { type: 'Identifier', name, metadata: { path: [] } },
975
+ metadata: { path: [] },
976
+ }));
977
+
978
+ program.body.unshift(
979
+ /** @type {any} */ ({
980
+ type: 'ImportDeclaration',
981
+ specifiers,
982
+ source: { type: 'Literal', value: 'solid-js', raw: "'solid-js'" },
983
+ metadata: { path: [] },
984
+ }),
985
+ );
986
+ }
987
+
988
+ // =====================================================================
989
+ // Element → JSX (with Solid-specific attribute handling)
990
+ // =====================================================================
991
+
992
+ /**
993
+ * @param {any} node
994
+ * @param {TransformContext} transform_context
995
+ * @returns {any}
996
+ */
997
+ function to_jsx_element(node, transform_context) {
998
+ if (node.type === 'JSXElement') return node;
999
+
1000
+ // `{html expr}` isn't supported on the Solid target — users should reach
1001
+ // for `innerHTML={...}` directly as an element attribute so the
1002
+ // semantics (replaces all children; only valid on host elements) are
1003
+ // explicit in their source. Only Ripple has a `{html ...}` primitive.
1004
+ // The check runs before the dynamic-element branch so `<@Dyn>{html x}</@Dyn>`
1005
+ // fails with the same diagnostic as the static-element case.
1006
+ const raw_children = node.children || [];
1007
+ if (raw_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
1008
+ throw new Error(
1009
+ '`{html ...}` is not supported on the Solid target. Use `innerHTML={...}` as an element attribute instead.',
1010
+ );
1011
+ }
1012
+
1013
+ if (is_dynamic_element_id(node.id)) {
1014
+ return dynamic_element_to_jsx_child(node, transform_context);
1015
+ }
1016
+
1017
+ const name = identifier_to_jsx_name(node.id);
1018
+ const is_composite = is_composite_element(node);
1019
+ const attributes = transform_element_attributes(
1020
+ node.attributes || [],
1021
+ is_composite,
1022
+ transform_context,
1023
+ );
1024
+
1025
+ // Optimization: `<el>{text expr}</el>` with a single `{text ...}` child
1026
+ // on a host (DOM) element lowers to `<el textContent={expr} />`. Solid
1027
+ // writes `textContent` as a direct DOM property, which is cheaper than
1028
+ // the `insert()`-based text node binding it would otherwise emit for
1029
+ // child expressions. Only safe when `{text ...}` is the sole child and
1030
+ // the parent is a host element (composite components receive
1031
+ // `textContent` as an opaque prop with no DOM semantics), and when the
1032
+ // user hasn't already set `textContent` themselves.
1033
+ let selfClosing = !!node.selfClosing;
1034
+ let children;
1035
+ if (
1036
+ !is_composite &&
1037
+ raw_children.length === 1 &&
1038
+ raw_children[0] &&
1039
+ raw_children[0].type === 'Text' &&
1040
+ !has_text_content_attribute(attributes)
1041
+ ) {
1042
+ const text_child = raw_children[0];
1043
+ attributes.push(
1044
+ set_loc(
1045
+ /** @type {any} */ ({
1046
+ type: 'JSXAttribute',
1047
+ name: {
1048
+ type: 'JSXIdentifier',
1049
+ name: 'textContent',
1050
+ metadata: { path: [] },
1051
+ },
1052
+ value: to_jsx_expression_container(
1053
+ to_text_expression(text_child.expression, text_child),
1054
+ text_child,
1055
+ ),
1056
+ shorthand: false,
1057
+ metadata: { path: [] },
1058
+ }),
1059
+ text_child,
1060
+ ),
1061
+ );
1062
+ children = [];
1063
+ selfClosing = true;
1064
+ } else {
1065
+ children = create_element_children(raw_children, transform_context);
1066
+ }
1067
+
1068
+ const openingElement = set_loc(
1069
+ /** @type {any} */ ({
1070
+ type: 'JSXOpeningElement',
1071
+ name,
1072
+ attributes,
1073
+ selfClosing,
1074
+ }),
1075
+ node.openingElement || node,
1076
+ );
1077
+
1078
+ const closingElement = selfClosing
1079
+ ? null
1080
+ : set_loc(
1081
+ /** @type {any} */ ({
1082
+ type: 'JSXClosingElement',
1083
+ name: clone_jsx_name(name, node.closingElement || node),
1084
+ }),
1085
+ node.closingElement || node,
1086
+ );
1087
+
1088
+ return set_loc(
1089
+ /** @type {any} */ ({
1090
+ type: 'JSXElement',
1091
+ openingElement,
1092
+ closingElement,
1093
+ children,
1094
+ }),
1095
+ node,
1096
+ );
1097
+ }
1098
+
1099
+ /**
1100
+ * @param {any[]} children
1101
+ * @param {TransformContext} transform_context
1102
+ * @returns {any[]}
1103
+ */
1104
+ function create_element_children(children, transform_context) {
1105
+ if (children.length === 0) return [];
1106
+ // Solid doesn't need React's hook-safe IIFE wrapping; every child is inline.
1107
+ return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
1108
+ }
1109
+
1110
+ /**
1111
+ * Attribute transform. Unlike React, Solid uses the native `class` attribute
1112
+ * (not `className`). `RefAttribute` and `SpreadAttribute` nodes are handled
1113
+ * at the element level by {@link transform_element_attributes} so this
1114
+ * function only sees plain attributes.
1115
+ *
1116
+ * @param {any} attr
1117
+ * @returns {any}
1118
+ */
1119
+ function to_jsx_attribute(attr) {
1120
+ if (!attr) return attr;
1121
+ if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') return attr;
1122
+
1123
+ const attr_name = attr.name;
1124
+ const name =
1125
+ attr_name && attr_name.type === 'Identifier' ? identifier_to_jsx_name(attr_name) : attr_name;
1126
+
1127
+ let value = attr.value;
1128
+ if (value) {
1129
+ if (value.type === 'Literal' && typeof value.value === 'string') {
1130
+ // Keep string literal as attribute string.
1131
+ } else if (value.type !== 'JSXExpressionContainer') {
1132
+ value = to_jsx_expression_container(value);
1133
+ }
1134
+ }
1135
+
1136
+ return set_loc(
1137
+ /** @type {any} */ ({
1138
+ type: 'JSXAttribute',
1139
+ name,
1140
+ value: value || null,
1141
+ shorthand: false,
1142
+ metadata: { path: [] },
1143
+ }),
1144
+ attr,
1145
+ );
1146
+ }
1147
+
1148
+ /**
1149
+ * @param {any} id
1150
+ * @returns {boolean}
1151
+ */
1152
+ function is_dynamic_element_id(id) {
1153
+ if (!id || typeof id !== 'object') return false;
1154
+ if (id.type === 'Identifier') return !!id.tracked;
1155
+ if (id.type === 'MemberExpression') return is_dynamic_element_id(id.object);
1156
+ return false;
1157
+ }
1158
+
1159
+ /**
1160
+ * Detect whether an `Element` node represents a composite component (tag
1161
+ * name starts with an uppercase letter, or is a member expression like
1162
+ * `Namespace.Component`).
1163
+ *
1164
+ * @param {any} node
1165
+ * @returns {boolean}
1166
+ */
1167
+ function is_composite_element(node) {
1168
+ const id = node?.id;
1169
+ if (!id) return false;
1170
+ if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
1171
+ if (id.type === 'MemberExpression') return true;
1172
+ return false;
1173
+ }
1174
+
1175
+ /**
1176
+ * Check if the user already supplied a `textContent` attribute on the
1177
+ * element, or if a spread attribute could supply one. If either is true the
1178
+ * compiler mustn't emit another `textContent` — the `{text expr}` →
1179
+ * `textContent={...}` optimization bails out. Spreads are treated as
1180
+ * potentially setting `textContent` because the spread's runtime shape
1181
+ * isn't knowable at compile time; emitting a second `textContent` attribute
1182
+ * would produce a duplicate-key conflict at runtime.
1183
+ *
1184
+ * @param {any[]} attributes
1185
+ * @returns {boolean}
1186
+ */
1187
+ function has_text_content_attribute(attributes) {
1188
+ return attributes.some(
1189
+ (/** @type {any} */ attr) =>
1190
+ attr &&
1191
+ ((attr.type === 'JSXAttribute' &&
1192
+ attr.name &&
1193
+ attr.name.type === 'JSXIdentifier' &&
1194
+ attr.name.name === 'textContent') ||
1195
+ attr.type === 'JSXSpreadAttribute'),
1196
+ );
1197
+ }
1198
+
1199
+ /**
1200
+ * Transform a list of raw attributes into JSX attributes, lifting
1201
+ * `{ref expr}` handling to the element level.
1202
+ *
1203
+ * `{ref expr}` compiles to `ref={expr}` on both DOM elements and composite
1204
+ * components. On DOM elements, Solid's JSX transform takes over: if `expr`
1205
+ * is a mutable `let`-declared identifier it assigns the element to the
1206
+ * variable; if `expr` is a function (or other callable) it invokes it
1207
+ * with the element. On composite components, `ref` is passed through as a
1208
+ * regular prop; the receiving child can consume it explicitly as
1209
+ * `props.ref` or spread `{...props}` onto a DOM element, where Solid's
1210
+ * spread runtime automatically applies the `ref` entry. Solid's merge
1211
+ * proxies drop Symbol keys, so the Symbol-based forwarding used by
1212
+ * Ripple doesn't port; the Solid target relies on its native `ref` prop
1213
+ * support instead.
1214
+ *
1215
+ * Multiple `{ref ...}` attributes on the same element are collected into
1216
+ * a single `ref={[a, b, ...]}` array so every callback fires. Solid's
1217
+ * ref/spread runtime (`applyRef`) already iterates array refs, so this
1218
+ * works on both DOM elements and composite components (when the child
1219
+ * spreads `props` or forwards `props.ref`).
1220
+ *
1221
+ * @param {any[]} raw_attrs
1222
+ * @param {boolean} is_composite
1223
+ * @param {TransformContext} transform_context
1224
+ * @returns {any[]}
1225
+ */
1226
+ function transform_element_attributes(raw_attrs, is_composite, transform_context) {
1227
+ void is_composite;
1228
+ void transform_context;
1229
+ /** @type {any[]} */
1230
+ const result = [];
1231
+ /** @type {any[]} */
1232
+ const ref_attrs = [];
1233
+
1234
+ for (const attr of raw_attrs) {
1235
+ if (!attr) continue;
1236
+ if (attr.type === 'RefAttribute') {
1237
+ ref_attrs.push(attr);
1238
+ continue;
1239
+ }
1240
+ if (attr.type === 'SpreadAttribute') {
1241
+ result.push(
1242
+ set_loc(
1243
+ /** @type {any} */ ({
1244
+ type: 'JSXSpreadAttribute',
1245
+ argument: attr.argument,
1246
+ }),
1247
+ attr,
1248
+ ),
1249
+ );
1250
+ continue;
1251
+ }
1252
+ result.push(to_jsx_attribute(attr));
1253
+ }
1254
+
1255
+ if (ref_attrs.length === 1) {
1256
+ result.push(build_ref_attribute(ref_attrs[0].argument, ref_attrs[0]));
1257
+ } else if (ref_attrs.length > 1) {
1258
+ const array_expr = /** @type {any} */ ({
1259
+ type: 'ArrayExpression',
1260
+ elements: ref_attrs.map((attr) => attr.argument),
1261
+ metadata: { path: [] },
1262
+ });
1263
+ result.push(build_ref_attribute(array_expr, ref_attrs[0]));
1264
+ }
1265
+
1266
+ return result;
1267
+ }
1268
+
1269
+ /**
1270
+ * Build a `ref={expr}` JSX attribute, passing the expression through
1271
+ * unchanged so Solid's JSX transform can apply its normal ref semantics.
1272
+ *
1273
+ * @param {any} argument
1274
+ * @param {any} source_node
1275
+ * @returns {any}
1276
+ */
1277
+ function build_ref_attribute(argument, source_node) {
1278
+ return set_loc(
1279
+ /** @type {any} */ ({
1280
+ type: 'JSXAttribute',
1281
+ name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
1282
+ value: to_jsx_expression_container(argument),
1283
+ shorthand: false,
1284
+ metadata: { path: [] },
1285
+ }),
1286
+ source_node,
1287
+ );
1288
+ }
1289
+
1290
+ /**
1291
+ * @param {any} node
1292
+ * @param {TransformContext} transform_context
1293
+ * @returns {any}
1294
+ */
1295
+ function dynamic_element_to_jsx_child(node, transform_context) {
1296
+ const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
1297
+ const alias_declaration = set_loc(
1298
+ /** @type {any} */ ({
1299
+ type: 'VariableDeclaration',
1300
+ kind: 'const',
1301
+ declarations: [
1302
+ {
1303
+ type: 'VariableDeclarator',
1304
+ id: dynamic_id,
1305
+ init: clone_expression_node(node.id),
1306
+ metadata: { path: [] },
1307
+ },
1308
+ ],
1309
+ metadata: { path: [] },
1310
+ }),
1311
+ node,
1312
+ );
1313
+ const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
1314
+
1315
+ return to_jsx_expression_container(
1316
+ /** @type {any} */ ({
1317
+ type: 'CallExpression',
1318
+ callee: {
1319
+ type: 'ArrowFunctionExpression',
1320
+ params: [],
1321
+ body: /** @type {any} */ ({
1322
+ type: 'BlockStatement',
1323
+ body: [
1324
+ alias_declaration,
1325
+ {
1326
+ type: 'ReturnStatement',
1327
+ argument: {
1328
+ type: 'ConditionalExpression',
1329
+ test: clone_identifier(dynamic_id),
1330
+ consequent: jsx_element,
1331
+ alternate: create_null_literal(),
1332
+ metadata: { path: [] },
1333
+ },
1334
+ metadata: { path: [] },
1335
+ },
1336
+ ],
1337
+ metadata: { path: [] },
1338
+ }),
1339
+ async: false,
1340
+ generator: false,
1341
+ expression: false,
1342
+ metadata: { path: [] },
1343
+ },
1344
+ arguments: [],
1345
+ optional: false,
1346
+ metadata: { path: [] },
1347
+ }),
1348
+ node,
1349
+ );
1350
+ }
1351
+
1352
+ /**
1353
+ * @param {AST.Identifier} dynamic_id
1354
+ * @param {any} node
1355
+ * @param {TransformContext} transform_context
1356
+ * @returns {any}
1357
+ */
1358
+ function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
1359
+ const is_composite = is_composite_element(node);
1360
+ const attributes = transform_element_attributes(
1361
+ node.attributes || [],
1362
+ is_composite,
1363
+ transform_context,
1364
+ );
1365
+ const selfClosing = !!node.selfClosing;
1366
+ const children = create_element_children(node.children || [], transform_context);
1367
+ const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
1368
+
1369
+ return /** @type {any} */ ({
1370
+ type: 'JSXElement',
1371
+ openingElement: {
1372
+ type: 'JSXOpeningElement',
1373
+ name,
1374
+ attributes,
1375
+ selfClosing,
1376
+ metadata: { path: [] },
1377
+ },
1378
+ closingElement: selfClosing
1379
+ ? null
1380
+ : {
1381
+ type: 'JSXClosingElement',
1382
+ name: clone_jsx_name(name),
1383
+ metadata: { path: [] },
1384
+ },
1385
+ children,
1386
+ metadata: { path: [] },
1387
+ });
1388
+ }
1389
+
1390
+ // =====================================================================
1391
+ // Text, expression, and helper utilities
1392
+ // =====================================================================
1393
+
1394
+ /**
1395
+ * @param {AST.Expression} expression
1396
+ * @param {any} [source_node]
1397
+ * @returns {any}
1398
+ */
1399
+ function to_jsx_expression_container(expression, source_node = expression) {
1400
+ return set_loc(
1401
+ /** @type {any} */ ({
1402
+ type: 'JSXExpressionContainer',
1403
+ expression: /** @type {any} */ (expression),
1404
+ metadata: { path: [] },
1405
+ }),
1406
+ source_node,
1407
+ );
1408
+ }
1409
+
1410
+ /**
1411
+ * `{text expr}` → `expr == null ? '' : expr + ''` — coerce to string,
1412
+ * matching React's text semantics so booleans/objects render as text.
1413
+ *
1414
+ * @param {AST.Expression} expression
1415
+ * @param {any} [source_node]
1416
+ * @returns {AST.Expression}
1417
+ */
1418
+ function to_text_expression(expression, source_node = expression) {
1419
+ return set_loc(
1420
+ /** @type {AST.Expression} */ ({
1421
+ type: 'ConditionalExpression',
1422
+ test: {
1423
+ type: 'BinaryExpression',
1424
+ operator: '==',
1425
+ left: clone_expression_node(expression),
1426
+ right: { type: 'Literal', value: null, raw: 'null', metadata: { path: [] } },
1427
+ metadata: { path: [] },
1428
+ },
1429
+ consequent: { type: 'Literal', value: '', raw: "''", metadata: { path: [] } },
1430
+ alternate: {
1431
+ type: 'BinaryExpression',
1432
+ operator: '+',
1433
+ left: clone_expression_node(expression),
1434
+ right: { type: 'Literal', value: '', raw: "''", metadata: { path: [] } },
1435
+ metadata: { path: [] },
1436
+ },
1437
+ metadata: { path: [] },
1438
+ }),
1439
+ source_node,
1440
+ );
1441
+ }
1442
+
1443
+ /**
1444
+ * @param {any[]} render_nodes
1445
+ * @returns {any}
1446
+ */
1447
+ function build_return_expression(render_nodes) {
1448
+ if (render_nodes.length === 0) return null;
1449
+ if (render_nodes.length === 1) {
1450
+ const only = render_nodes[0];
1451
+ if (only.type === 'JSXExpressionContainer') return only.expression;
1452
+ return only;
1453
+ }
1454
+ const first = render_nodes[0];
1455
+ const last = render_nodes[render_nodes.length - 1];
1456
+ return set_loc(
1457
+ /** @type {any} */ ({
1458
+ type: 'JSXFragment',
1459
+ openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
1460
+ closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
1461
+ children: render_nodes,
1462
+ metadata: { path: [] },
1463
+ }),
1464
+ first?.loc && last?.loc
1465
+ ? { start: first.start, end: last.end, loc: { start: first.loc.start, end: last.loc.end } }
1466
+ : undefined,
1467
+ );
1468
+ }
1469
+
1470
+ /**
1471
+ * @param {AST.Identifier | AST.MemberExpression | any} id
1472
+ * @returns {any}
1473
+ */
1474
+ function identifier_to_jsx_name(id) {
1475
+ if (id.type === 'Identifier') {
1476
+ return set_loc(
1477
+ /** @type {any} */ ({
1478
+ type: 'JSXIdentifier',
1479
+ name: id.name,
1480
+ metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
1481
+ }),
1482
+ id,
1483
+ );
1484
+ }
1485
+ if (id.type === 'MemberExpression') {
1486
+ return set_loc(
1487
+ /** @type {any} */ ({
1488
+ type: 'JSXMemberExpression',
1489
+ object: /** @type {any} */ (identifier_to_jsx_name(id.object)),
1490
+ property: /** @type {any} */ (identifier_to_jsx_name(id.property)),
1491
+ }),
1492
+ id,
1493
+ );
1494
+ }
1495
+ return id;
1496
+ }
1497
+
1498
+ /**
1499
+ * @param {any} name
1500
+ * @param {any} [source_node]
1501
+ * @returns {any}
1502
+ */
1503
+ function clone_jsx_name(name, source_node = name) {
1504
+ if (name.type === 'JSXIdentifier') {
1505
+ return set_loc(
1506
+ { type: 'JSXIdentifier', name: name.name, metadata: name.metadata || { path: [] } },
1507
+ source_node,
1508
+ );
1509
+ }
1510
+ if (name.type === 'JSXMemberExpression') {
1511
+ return set_loc(
1512
+ {
1513
+ type: 'JSXMemberExpression',
1514
+ object: clone_jsx_name(name.object, source_node.object || name.object),
1515
+ property: clone_jsx_name(name.property, source_node.property || name.property),
1516
+ metadata: name.metadata || { path: [] },
1517
+ },
1518
+ source_node,
1519
+ );
1520
+ }
1521
+ return name;
1522
+ }
1523
+
1524
+ /**
1525
+ * @param {AST.Identifier} identifier
1526
+ * @returns {any}
1527
+ */
1528
+ function clone_identifier(identifier) {
1529
+ return set_loc(
1530
+ /** @type {any} */ ({
1531
+ type: 'Identifier',
1532
+ name: identifier.name,
1533
+ metadata: { path: [] },
1534
+ }),
1535
+ identifier,
1536
+ );
1537
+ }
1538
+
1539
+ /**
1540
+ * @param {any} node
1541
+ * @returns {any}
1542
+ */
1543
+ function clone_expression_node(node) {
1544
+ if (!node || typeof node !== 'object') return node;
1545
+ if (Array.isArray(node)) return node.map(clone_expression_node);
1546
+ const clone = { ...node };
1547
+ for (const key of Object.keys(clone)) {
1548
+ // Positional keys are value-shared across nodes and — more importantly —
1549
+ // `loc` objects often contain back-references to sub-objects that would
1550
+ // blow the stack without a cycle guard. Every other AST traversal in
1551
+ // this file skips these; keep them as shallow-copied references.
1552
+ if (key === 'loc' || key === 'start' || key === 'end') continue;
1553
+ if (key === 'metadata') {
1554
+ clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
1555
+ continue;
1556
+ }
1557
+ clone[key] = clone_expression_node(clone[key]);
1558
+ }
1559
+ return clone;
1560
+ }
1561
+
1562
+ /**
1563
+ * @returns {AST.Literal}
1564
+ */
1565
+ function create_null_literal() {
1566
+ return /** @type {any} */ ({ type: 'Literal', value: null, raw: 'null', metadata: { path: [] } });
1567
+ }
1568
+
1569
+ /**
1570
+ * @template T
1571
+ * @param {T} node
1572
+ * @param {any} source_node
1573
+ * @returns {T}
1574
+ */
1575
+ function set_loc(node, source_node) {
1576
+ /** @type {any} */ (node).metadata ??= { path: [] };
1577
+ if (source_node?.loc) {
1578
+ return /** @type {T} */ (setLocation(/** @type {any} */ (node), source_node, true));
1579
+ }
1580
+ return node;
1581
+ }
1582
+
1583
+ /**
1584
+ * @param {any} left
1585
+ * @param {any} index
1586
+ * @returns {any[]}
1587
+ */
1588
+ function get_for_of_iteration_params(left, index) {
1589
+ const params = [];
1590
+ if (left?.type === 'VariableDeclaration') {
1591
+ params.push(left.declarations[0]?.id);
1592
+ } else {
1593
+ params.push(left);
1594
+ }
1595
+ if (index) params.push(index);
1596
+ return params;
1597
+ }
1598
+
1599
+ /**
1600
+ * @param {string} name
1601
+ * @returns {any}
1602
+ */
1603
+ function create_generated_identifier(name) {
1604
+ return /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
1605
+ }
1606
+
1607
+ /**
1608
+ * @param {any} node
1609
+ * @param {string} message
1610
+ * @returns {Error & { pos: number, end: number }}
1611
+ */
1612
+ function create_compile_error(node, message) {
1613
+ const error = /** @type {Error & { pos: number, end: number }} */ (new Error(message));
1614
+ error.pos = node.start ?? 0;
1615
+ error.end = node.end ?? error.pos + 1;
1616
+ return error;
1617
+ }
1618
+
1619
+ /**
1620
+ * @param {any[]} consequent
1621
+ * @returns {any[]}
1622
+ */
1623
+ function flatten_switch_consequent(consequent) {
1624
+ const result = [];
1625
+ for (const node of consequent) {
1626
+ if (node.type === 'BlockStatement') result.push(...node.body);
1627
+ else result.push(node);
1628
+ }
1629
+ return result;
1630
+ }
1631
+
1632
+ /**
1633
+ * @param {any} node
1634
+ * @returns {any}
1635
+ */
1636
+ function tsx_compat_node_to_jsx_expression(node) {
1637
+ if (node.kind !== 'solid') {
1638
+ throw create_compile_error(
1639
+ node,
1640
+ `Solid TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or <tsx:solid>.`,
1641
+ );
1642
+ }
1643
+ return tsx_node_to_jsx_expression(node);
1644
+ }
1645
+
1646
+ /**
1647
+ * `<tsx>...</tsx>` → Solid JSX fragment (or single child if only one).
1648
+ *
1649
+ * @param {any} node
1650
+ * @returns {any}
1651
+ */
1652
+ function tsx_node_to_jsx_expression(node) {
1653
+ const children = (node.children || []).filter(
1654
+ (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
1655
+ );
1656
+
1657
+ if (children.length === 1 && children[0].type !== 'JSXText') {
1658
+ return strip_locations(children[0]);
1659
+ }
1660
+
1661
+ return strip_locations(
1662
+ /** @type {any} */ ({
1663
+ type: 'JSXFragment',
1664
+ openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
1665
+ closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
1666
+ children,
1667
+ metadata: { path: [] },
1668
+ }),
1669
+ );
1670
+ }
1671
+
1672
+ /**
1673
+ * @param {any} node
1674
+ * @returns {any}
1675
+ */
1676
+ function strip_locations(node) {
1677
+ if (!node || typeof node !== 'object') return node;
1678
+ if (Array.isArray(node)) return node.map(strip_locations);
1679
+ delete node.loc;
1680
+ delete node.start;
1681
+ delete node.end;
1682
+ for (const key of Object.keys(node)) {
1683
+ if (key === 'metadata') continue;
1684
+ node[key] = strip_locations(node[key]);
1685
+ }
1686
+ return node;
1687
+ }