@tsrx/core 0.1.19 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,8 +24,8 @@ import {
24
24
  identifier_to_jsx_name,
25
25
  is_bare_render_expression,
26
26
  is_component_jsx_name,
27
- is_dynamic_element_id,
28
27
  is_jsx_child,
28
+ jsx_name_to_expression,
29
29
  set_loc,
30
30
  to_text_expression,
31
31
  } from './ast-builders.js';
@@ -62,7 +62,18 @@ const HOOK_OUTER_ASSIGNMENT_ERROR =
62
62
  const HOOK_CALLBACK_OUTER_MUTATION_ERROR =
63
63
  'Hook callbacks inside conditional or repeated TSRX scopes must not mutate bindings declared outside the generated hook component.';
64
64
  const TEMPLATE_FRAGMENT_ERROR =
65
- 'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
65
+ 'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only in expression position.';
66
+ const TSRX_FOR_RETURN_ERROR =
67
+ 'Return statements are not allowed inside TSRX template for...of loops. Filter the iterable before rendering or use an @for empty fallback for empty lists.';
68
+ const TSRX_FOR_BREAK_ERROR =
69
+ 'Break statements are not allowed inside TSRX template for...of loops.';
70
+ const TSRX_FOR_CONTINUE_ERROR =
71
+ 'Continue statements are not allowed inside TSRX template for...of loops. Filter the iterable before rendering.';
72
+ const TSRX_IF_RETURN_ERROR =
73
+ 'Return statements are not allowed inside TSRX template @if blocks. Move the return before the template output or render conditionally instead.';
74
+ const TSRX_IF_BREAK_ERROR = 'Break statements are not allowed inside TSRX template @if blocks.';
75
+ const TSRX_IF_CONTINUE_ERROR =
76
+ 'Continue statements are not allowed inside TSRX template @if blocks. Filter before rendering or use conditional output instead.';
66
77
 
67
78
  /**
68
79
  * @param {AST.Node} node
@@ -142,6 +153,76 @@ function is_function_or_class_boundary(node) {
142
153
  );
143
154
  }
144
155
 
156
+ /**
157
+ * @param {any} node
158
+ * @param {boolean} [inside_function]
159
+ * @param {Set<any>} [seen]
160
+ * @returns {void}
161
+ */
162
+ function mark_nested_function_return_jsx(node, inside_function = false, seen = new Set()) {
163
+ if (!node || typeof node !== 'object' || seen.has(node)) return;
164
+ seen.add(node);
165
+
166
+ if (Array.isArray(node)) {
167
+ for (const item of node) mark_nested_function_return_jsx(item, inside_function, seen);
168
+ return;
169
+ }
170
+
171
+ const now_inside = inside_function || is_function_or_class_boundary(node);
172
+
173
+ if (
174
+ now_inside &&
175
+ node.type === 'ReturnStatement' &&
176
+ (node.argument?.type === 'JSXFragment' ||
177
+ node.argument?.type === 'JSXElement' ||
178
+ node.argument?.type === 'JSXStyleElement')
179
+ ) {
180
+ node.argument.metadata = { ...(node.argument.metadata || {}), native_tsrx: true };
181
+ }
182
+
183
+ for (const key of Object.keys(node)) {
184
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
185
+ mark_nested_function_return_jsx(node[key], now_inside, seen);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Flatten a `@{ … }` code block that appears as an element/fragment child into
191
+ * the element's children list: its setup statements followed by its single
192
+ * render output. The render pipeline already handles interleaved setup
193
+ * statements and JSX children. This is the element-scoped equivalent of
194
+ * `transform_function`'s body lowering — function and arrow bodies are never
195
+ * element children, so they are untouched here.
196
+ * @param {any} node
197
+ * @param {Set<any>} [seen]
198
+ * @returns {void}
199
+ */
200
+ function expand_child_code_blocks(node, seen = new Set()) {
201
+ if (!node || typeof node !== 'object' || seen.has(node)) return;
202
+ seen.add(node);
203
+
204
+ if (Array.isArray(node)) {
205
+ for (const item of node) expand_child_code_blocks(item, seen);
206
+ return;
207
+ }
208
+
209
+ if (
210
+ Array.isArray(node.children) &&
211
+ node.children.some((/** @type {any} */ c) => c?.type === 'JSXCodeBlock')
212
+ ) {
213
+ node.children = node.children.flatMap((/** @type {any} */ child) =>
214
+ child?.type === 'JSXCodeBlock'
215
+ ? [...child.body, ...(child.render != null ? [child.render] : [])]
216
+ : [child],
217
+ );
218
+ }
219
+
220
+ for (const key of Object.keys(node)) {
221
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
222
+ expand_child_code_blocks(node[key], seen);
223
+ }
224
+ }
225
+
145
226
  /**
146
227
  * Build a `transform()` function for a specific JSX platform (React, Preact,
147
228
  * Solid). Given a `JsxPlatform` descriptor, returns a transform that lowers
@@ -150,7 +231,7 @@ function is_function_or_class_boundary(node) {
150
231
  * Any `<style>` element declared inside a TSRX fragment is collected, rendered
151
232
  * via `@tsrx/core`'s stylesheet renderer, and returned alongside the JS output
152
233
  * so a downstream plugin can inject it. The compiler also augments every
153
- * non-style Element in that fragment with the stylesheet's hash class so scoped
234
+ * non-style JSX element in that fragment with the stylesheet's hash class so scoped
154
235
  * selectors match correctly.
155
236
  *
156
237
  * @param {JsxPlatform} platform
@@ -203,6 +284,8 @@ export function createJsxTransform(platform) {
203
284
  ...(platform.hooks?.initialState?.() ?? {}),
204
285
  };
205
286
 
287
+ expand_child_code_blocks(/** @type {any} */ (ast));
288
+
206
289
  if (!transform_context.typeOnly) {
207
290
  preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
208
291
  }
@@ -213,15 +296,11 @@ export function createJsxTransform(platform) {
213
296
  return next();
214
297
  },
215
298
 
216
- Tsx(node, { next, path }) {
217
- const inner = /** @type {any} */ (next() ?? node);
218
- const in_jsx_child = in_jsx_child_context(path);
219
- return /** @type {any} */ (
220
- wrap_jsx_setup_declarations(tsx_node_to_jsx_expression(inner, in_jsx_child), in_jsx_child)
221
- );
222
- },
299
+ JSXFragment(node, { next, path, state, visit }) {
300
+ if (!node.metadata?.native_tsrx) {
301
+ return next() ?? node;
302
+ }
223
303
 
224
- Tsrx(node, { next, path, state, visit }) {
225
304
  const parent = /** @type {AST.ArrowFunctionExpression} */ (path.at(-1));
226
305
  if (parent?.metadata?.native_tsrx && parent.body === node) {
227
306
  return /** @type {any} */ (visit(create_native_tsrx_render_block(node, state), state));
@@ -241,18 +320,15 @@ export function createJsxTransform(platform) {
241
320
  return /** @type {any} */ (wrap_jsx_setup_declarations(expression, in_jsx_child));
242
321
  },
243
322
 
244
- TsxCompat(node, { next, path, state }) {
245
- const inner = /** @type {any} */ (next() ?? node);
246
- const in_jsx_child = in_jsx_child_context(path);
247
- return /** @type {any} */ (
248
- wrap_jsx_setup_declarations(
249
- tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child),
250
- in_jsx_child,
251
- )
252
- );
253
- },
323
+ JSXElement(node, { next, path, state }) {
324
+ if (!node.metadata?.native_tsrx && is_dynamic_jsx_element(node)) {
325
+ return dynamic_element_to_jsx(node, state, in_jsx_child_context(path));
326
+ }
327
+
328
+ if (!node.metadata?.native_tsrx) {
329
+ return next() ?? node;
330
+ }
254
331
 
255
- Element(node, { next, path, state }) {
256
332
  if (is_style_element(node) && is_style_expression_position(path)) {
257
333
  const stylesheet = get_style_element_stylesheet(node);
258
334
  if (stylesheet) {
@@ -262,30 +338,57 @@ export function createJsxTransform(platform) {
262
338
  }
263
339
  }
264
340
 
265
- // Capture raw children BEFORE the walker transforms them so a
266
- // platform hook (e.g. Solid's textContent optimization) can
267
- // inspect the original Text / TSRXExpression nodes rather than
268
- // their walker-lowered JSXExpressionContainer equivalents.
341
+ // Capture raw children BEFORE the walker transforms them so platform
342
+ // hooks can inspect the original JSX child shape.
269
343
  const raw_children = /** @type {any} */ (node.children || []).map(
270
344
  (/** @type {any} */ child) => (child && typeof child === 'object' ? { ...child } : child),
271
345
  );
272
346
  const inner = /** @type {any} */ (next() ?? node);
273
347
  const hook = platform.hooks?.transformElement;
274
348
  if (hook) return /** @type {any} */ (hook(inner, state, raw_children));
275
- return /** @type {any} */ (to_jsx_element(inner, state, raw_children));
349
+ return /** @type {any} */ (
350
+ to_jsx_element(inner, state, raw_children, in_jsx_child_context(path))
351
+ );
276
352
  },
277
353
 
278
- Text(node, { next }) {
279
- const inner = /** @type {any} */ (next() ?? node);
354
+ JSXExpressionContainer(node, { next, state }) {
355
+ const result = /** @type {any} */ (next() ?? node);
356
+ const expression = result.expression;
357
+ // `@if`/`@for`/`@switch`/`@try` used as an expression value (e.g. an
358
+ // attribute value `content={@if (…) { … }}` or a `{ … }` child) leaks a
359
+ // JSX*Expression node straight to the printer. Lower it with the same
360
+ // control-flow machinery used for render children and unwrap the value.
361
+ if (
362
+ is_if_control_node(expression) ||
363
+ is_switch_control_node(expression) ||
364
+ is_try_control_node(expression) ||
365
+ expression?.type === 'JSXForExpression'
366
+ ) {
367
+ const lowered = /** @type {any} */ (to_jsx_child(expression, state));
368
+ return { ...result, expression: lowered?.expression ?? lowered };
369
+ }
370
+ return result;
371
+ },
372
+
373
+ JSXStyleElement(node, { path, state }) {
374
+ if (is_style_expression_position(path)) {
375
+ const stylesheet = get_style_element_stylesheet(node);
376
+ if (stylesheet) {
377
+ analyze_css(stylesheet);
378
+ state.stylesheets.push(stylesheet);
379
+ return /** @type {any} */ (create_style_expression_value(node, stylesheet, state));
380
+ }
381
+ }
280
382
  return /** @type {any} */ (
281
- to_jsx_expression_container(to_text_expression(inner.expression, inner), inner)
383
+ b.jsx_element(
384
+ /** @type {ESTreeJSX.JSXElement} */ ({ ...node, type: 'JSXElement', children: [] }),
385
+ node.openingElement?.attributes ?? [],
386
+ [],
387
+ )
282
388
  );
283
389
  },
284
390
 
285
- TSRXExpression(node, { next }) {
286
- const inner = /** @type {any} */ (next() ?? node);
287
- return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
288
- },
391
+ JSXCodeBlock: transform_jsx_code_block,
289
392
 
290
393
  BlockStatement: transform_block_statement,
291
394
  ReturnStatement: transform_return_statement,
@@ -338,6 +441,7 @@ export function createJsxTransform(platform) {
338
441
  ? expanded
339
442
  : apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
340
443
  );
444
+ lower_remaining_jsx_code_blocks(final_program, transform_context);
341
445
 
342
446
  const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
343
447
  sourceMapSource: filename,
@@ -416,7 +520,7 @@ function collect_css_prunable_elements(value, elements = []) {
416
520
  return elements;
417
521
  }
418
522
 
419
- if (value.type === 'Element') {
523
+ if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
420
524
  if (!is_style_element(value)) {
421
525
  elements.push(value);
422
526
  }
@@ -448,6 +552,12 @@ function build_component_statements(body_nodes, transform_context) {
448
552
  * @returns {any[]}
449
553
  */
450
554
  function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
555
+ body_nodes = body_nodes.flatMap((node) =>
556
+ node?.type === 'JSXCodeBlock'
557
+ ? [...node.body, ...(node.render != null ? [node.render] : [])]
558
+ : [node],
559
+ );
560
+
451
561
  const statements = [];
452
562
  const render_nodes = [];
453
563
  let has_terminal_return = false;
@@ -530,7 +640,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
530
640
  }
531
641
 
532
642
  if (
533
- child.type === 'ForOfStatement' &&
643
+ is_for_of_control_node(child) &&
534
644
  !child.await &&
535
645
  should_extract_hook_helpers(transform_context) &&
536
646
  !transform_context.platform.hooks?.isTopLevelSetupCall &&
@@ -541,7 +651,10 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
541
651
  true,
542
652
  )
543
653
  ) {
544
- const hoisted = build_hoisted_for_of_with_hooks(child, transform_context);
654
+ const hoisted = build_hoisted_for_of_with_hooks(
655
+ jsx_control_expression_to_statement(child),
656
+ transform_context,
657
+ );
545
658
  if (hoisted) {
546
659
  statements.push(...hoisted.hoist_statements);
547
660
  if (interleaved && is_capturable_jsx_child(hoisted.jsx_child)) {
@@ -568,6 +681,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
568
681
  } else if (is_bare_render_expression(child)) {
569
682
  render_nodes.push(to_jsx_expression_container(child, child));
570
683
  } else {
684
+ mark_nested_function_return_jsx(child);
571
685
  statements.push(child);
572
686
  collect_statement_bindings(child, transform_context.available_bindings);
573
687
  }
@@ -675,7 +789,7 @@ function find_hook_split_index(body_nodes, transform_context) {
675
789
  * @returns {boolean}
676
790
  */
677
791
  function is_component_body_conditional_return_statement(node) {
678
- if (node?.type !== 'IfStatement') {
792
+ if (!is_if_control_node(node)) {
679
793
  return false;
680
794
  }
681
795
 
@@ -710,20 +824,20 @@ function statement_contains_component_body_return(node) {
710
824
  return (node.body || []).some(statement_contains_component_body_return);
711
825
  }
712
826
 
713
- if (node.type === 'IfStatement') {
827
+ if (is_if_control_node(node)) {
714
828
  return (
715
829
  statement_contains_component_body_return(node.consequent) ||
716
830
  statement_contains_component_body_return(node.alternate)
717
831
  );
718
832
  }
719
833
 
720
- if (node.type === 'SwitchStatement') {
834
+ if (is_switch_control_node(node)) {
721
835
  return (node.cases || []).some((/** @type {any} */ switch_case) =>
722
836
  statement_contains_component_body_return(switch_case.consequent || []),
723
837
  );
724
838
  }
725
839
 
726
- if (node.type === 'TryStatement') {
840
+ if (is_try_control_node(node)) {
727
841
  return (
728
842
  statement_contains_component_body_return(node.block) ||
729
843
  statement_contains_component_body_return(node.handler?.body) ||
@@ -1016,13 +1130,41 @@ function transform_block_statement(node, { next, visit, state, path }) {
1016
1130
  * @returns {any}
1017
1131
  */
1018
1132
  function transform_return_statement(node, { next, visit, state, path }) {
1019
- if (get_active_native_tsrx_function(path) && node.argument?.type === 'Tsrx') {
1133
+ if (get_active_native_tsrx_function(path) && is_native_tsrx_node(node.argument)) {
1020
1134
  return visit(create_native_tsrx_render_block(node.argument, state), state);
1021
1135
  }
1022
1136
 
1023
1137
  return next() ?? node;
1024
1138
  }
1025
1139
 
1140
+ /**
1141
+ * @param {any} node
1142
+ * @param {{ state: TransformContext, path: AST.Node[] }} context
1143
+ * @returns {any}
1144
+ */
1145
+ function transform_jsx_code_block(node, { state, path }) {
1146
+ const body_nodes = get_jsx_code_block_body_nodes(node, state);
1147
+ const parent = /** @type {any} */ (path.at(-1));
1148
+
1149
+ if (parent && parent.body === node && is_function_or_class_boundary(parent)) {
1150
+ const block = b.block(
1151
+ mark_native_pretransformed_jsx(build_render_statements(body_nodes, true, state)),
1152
+ node,
1153
+ );
1154
+ block.metadata = {
1155
+ ...(block.metadata || {}),
1156
+ native_return_block: true,
1157
+ };
1158
+ return block;
1159
+ }
1160
+
1161
+ const expression = b.call(
1162
+ b.arrow([], b.block(build_render_statements(body_nodes, true, state), node)),
1163
+ );
1164
+
1165
+ return in_jsx_child_context(path) ? to_jsx_expression_container(expression, node) : expression;
1166
+ }
1167
+
1026
1168
  /**
1027
1169
  * @param {AST.Node[]} path
1028
1170
  * @returns {any | null}
@@ -1043,6 +1185,13 @@ function get_active_native_tsrx_function(path) {
1043
1185
  * @returns {any}
1044
1186
  */
1045
1187
  function transform_function(node, context) {
1188
+ // Lower a `@{ … }` function body (JSXCodeBlock) to an ordinary block: the
1189
+ // setup statements followed by `return <render>` when the block produces a
1190
+ // render output. The parser already marks the render JSX as native_tsrx, so
1191
+ // from here it flows through the existing native-component machinery exactly
1192
+ // like the older fenced `{ return <> … </> }` shape.
1193
+ lower_jsx_code_block_function_body(node);
1194
+
1046
1195
  if (node.metadata?.native_tsrx_function || function_has_native_tsrx_return(node)) {
1047
1196
  return transform_native_tsrx_function(node, context);
1048
1197
  }
@@ -1050,6 +1199,34 @@ function transform_function(node, context) {
1050
1199
  return transform_function_with_hook_helpers(node, context);
1051
1200
  }
1052
1201
 
1202
+ /**
1203
+ * @param {any} node
1204
+ * @returns {void}
1205
+ */
1206
+ function lower_jsx_code_block_function_body(node) {
1207
+ if (node.body?.type !== 'JSXCodeBlock') return;
1208
+
1209
+ const code_block = node.body;
1210
+ const statements = [...code_block.body];
1211
+ if (code_block.render != null) {
1212
+ let render = code_block.render;
1213
+ if (!is_native_tsrx_node(render)) {
1214
+ // A control-flow output (@if/@for/@switch/@try) isn't itself a native
1215
+ // template node, so `return @if (…) { … }` wouldn't be recognized as a
1216
+ // component render output. Wrap it in a native fragment so it flows
1217
+ // through the same children-rendering path as a `<> … </>` render.
1218
+ const fragment = b.jsx_fragment([render]);
1219
+ fragment.metadata = { ...fragment.metadata, native_tsrx: true };
1220
+ render = fragment;
1221
+ }
1222
+ statements.push(b.return(render, code_block.render));
1223
+ }
1224
+ node.body = b.block(statements, code_block);
1225
+ if (node.type === 'ArrowFunctionExpression') {
1226
+ node.expression = false;
1227
+ }
1228
+ }
1229
+
1053
1230
  /**
1054
1231
  * @param {any} node
1055
1232
  * @param {{ next: () => any, state: TransformContext }} context
@@ -1080,7 +1257,7 @@ function transform_native_tsrx_function(node, { next, state }) {
1080
1257
  if (
1081
1258
  inner !== node &&
1082
1259
  node.type === 'ArrowFunctionExpression' &&
1083
- node.body?.type === 'Tsrx' &&
1260
+ is_native_tsrx_node(node.body) &&
1084
1261
  inner.body?.type === 'BlockStatement'
1085
1262
  ) {
1086
1263
  inner.expression = false;
@@ -1140,6 +1317,10 @@ function find_native_await(node) {
1140
1317
  return find_first_top_level_await(node.body, false);
1141
1318
  }
1142
1319
 
1320
+ if (node.body?.type === 'JSXCodeBlock') {
1321
+ return find_native_await_in_list(get_raw_jsx_code_block_body_nodes(node.body));
1322
+ }
1323
+
1143
1324
  const body = node.body?.type === 'BlockStatement' ? node.body.body || [] : [];
1144
1325
  return find_native_await_in_list(body);
1145
1326
  }
@@ -1163,7 +1344,7 @@ function find_native_await_in_list(statements) {
1163
1344
  function find_native_await_in_statement(statement) {
1164
1345
  if (!statement || typeof statement !== 'object') return null;
1165
1346
 
1166
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'Tsrx') {
1347
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1167
1348
  return find_first_top_level_await_in_tsrx_function_body(statement.argument.children || []);
1168
1349
  }
1169
1350
 
@@ -1182,14 +1363,14 @@ function find_native_await_in_statement(statement) {
1182
1363
  return find_native_await_in_list(statement.body || []);
1183
1364
  }
1184
1365
 
1185
- if (statement.type === 'IfStatement') {
1366
+ if (is_if_control_node(statement)) {
1186
1367
  return (
1187
1368
  find_native_await_in_statement(statement.consequent) ||
1188
1369
  find_native_await_in_statement(statement.alternate)
1189
1370
  );
1190
1371
  }
1191
1372
 
1192
- if (statement.type === 'SwitchStatement') {
1373
+ if (is_switch_control_node(statement)) {
1193
1374
  for (const switch_case of statement.cases || []) {
1194
1375
  const found = find_native_await_in_list(switch_case.consequent || []);
1195
1376
  if (found) return found;
@@ -1197,7 +1378,7 @@ function find_native_await_in_statement(statement) {
1197
1378
  return null;
1198
1379
  }
1199
1380
 
1200
- if (statement.type === 'TryStatement') {
1381
+ if (is_try_control_node(statement)) {
1201
1382
  return (
1202
1383
  find_native_await_in_statement(statement.block) ||
1203
1384
  find_native_await_in_statement(statement.handler?.body) ||
@@ -1205,7 +1386,7 @@ function find_native_await_in_statement(statement) {
1205
1386
  );
1206
1387
  }
1207
1388
 
1208
- return null;
1389
+ return find_first_top_level_await(statement, false);
1209
1390
  }
1210
1391
 
1211
1392
  /**
@@ -1258,7 +1439,7 @@ function transform_function_with_hook_helpers(node, { next, state }) {
1258
1439
  * @returns {string}
1259
1440
  */
1260
1441
  function get_function_helper_base_name(node) {
1261
- return get_function_like_name(node) || 'Tsrx';
1442
+ return get_function_like_name(node) || 'TSRXTemplate';
1262
1443
  }
1263
1444
 
1264
1445
  /**
@@ -1337,7 +1518,7 @@ function collect_function_scope_bindings(node) {
1337
1518
  const bindings = collect_param_bindings(node.params || []);
1338
1519
  if (node.body?.type === 'BlockStatement') {
1339
1520
  for (const statement of node.body.body || []) {
1340
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'Tsrx') {
1521
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1341
1522
  for (const child of get_tsrx_render_children(statement.argument)) {
1342
1523
  collect_statement_bindings(child, bindings);
1343
1524
  }
@@ -1369,6 +1550,10 @@ function merge_binding_maps(outer, inner) {
1369
1550
  function function_has_native_tsrx_return(node) {
1370
1551
  if (!node) return false;
1371
1552
 
1553
+ if (node.body?.type === 'JSXCodeBlock') {
1554
+ return true;
1555
+ }
1556
+
1372
1557
  if (node.type === 'ArrowFunctionExpression' && node.body?.type !== 'BlockStatement') {
1373
1558
  return node_contains_native_tsrx_template(node.body);
1374
1559
  }
@@ -1404,22 +1589,23 @@ function statement_contains_native_tsrx_return(statement) {
1404
1589
  return statements_contain_native_tsrx_return(statement.body || []);
1405
1590
  }
1406
1591
 
1407
- if (statement.type === 'IfStatement') {
1592
+ if (is_if_control_node(statement)) {
1408
1593
  return (
1409
1594
  statement_contains_native_tsrx_return(statement.consequent) ||
1410
1595
  statement_contains_native_tsrx_return(statement.alternate)
1411
1596
  );
1412
1597
  }
1413
1598
 
1414
- if (statement.type === 'SwitchStatement') {
1599
+ if (is_switch_control_node(statement)) {
1415
1600
  return (statement.cases || []).some((/** @type {any} */ c) =>
1416
1601
  statements_contain_native_tsrx_return(c.consequent || []),
1417
1602
  );
1418
1603
  }
1419
1604
 
1420
- if (statement.type === 'TryStatement') {
1605
+ if (is_try_control_node(statement)) {
1421
1606
  return (
1422
1607
  statement_contains_native_tsrx_return(statement.block) ||
1608
+ statement_contains_native_tsrx_return(statement.pending) ||
1423
1609
  statement_contains_native_tsrx_return(statement.handler?.body) ||
1424
1610
  statement_contains_native_tsrx_return(statement.finalizer)
1425
1611
  );
@@ -1446,7 +1632,7 @@ function statement_contains_native_tsrx_return(statement) {
1446
1632
  */
1447
1633
  function node_contains_native_tsrx_template(node) {
1448
1634
  if (!node || typeof node !== 'object') return false;
1449
- if (node.type === 'Element' || node.type === 'Tsrx') return true;
1635
+ if (is_native_tsrx_node(node)) return true;
1450
1636
 
1451
1637
  if (is_function_or_class_boundary(node)) {
1452
1638
  return false;
@@ -1637,11 +1823,11 @@ function collect_style_elements(node, styles) {
1637
1823
  return;
1638
1824
  }
1639
1825
 
1640
- if (is_function_or_class_boundary(node) || node.type === 'Tsrx') {
1826
+ if (is_function_or_class_boundary(node)) {
1641
1827
  return;
1642
1828
  }
1643
1829
 
1644
- if (node.type === 'Element') {
1830
+ if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
1645
1831
  collect_style_elements(node.children || [], styles);
1646
1832
  return;
1647
1833
  }
@@ -1651,20 +1837,20 @@ function collect_style_elements(node, styles) {
1651
1837
  return;
1652
1838
  }
1653
1839
 
1654
- if (node.type === 'IfStatement') {
1840
+ if (is_if_control_node(node)) {
1655
1841
  collect_style_elements(node.consequent, styles);
1656
1842
  collect_style_elements(node.alternate, styles);
1657
1843
  return;
1658
1844
  }
1659
1845
 
1660
- if (node.type === 'SwitchStatement') {
1846
+ if (is_switch_control_node(node)) {
1661
1847
  for (const switch_case of node.cases || []) {
1662
1848
  collect_style_elements(switch_case.consequent || [], styles);
1663
1849
  }
1664
1850
  return;
1665
1851
  }
1666
1852
 
1667
- if (node.type === 'TryStatement') {
1853
+ if (is_try_control_node(node)) {
1668
1854
  collect_style_elements(node.block, styles);
1669
1855
  collect_style_elements(node.handler?.body, styles);
1670
1856
  collect_style_elements(node.finalizer, styles);
@@ -1716,7 +1902,7 @@ function strip_style_elements(node) {
1716
1902
  return node;
1717
1903
  }
1718
1904
 
1719
- if (node.type === 'Element') {
1905
+ if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
1720
1906
  node.children = strip_style_elements(node.children || []);
1721
1907
  return node;
1722
1908
  }
@@ -1726,20 +1912,20 @@ function strip_style_elements(node) {
1726
1912
  return node;
1727
1913
  }
1728
1914
 
1729
- if (node.type === 'IfStatement') {
1915
+ if (is_if_control_node(node)) {
1730
1916
  node.consequent = strip_style_elements(node.consequent);
1731
1917
  if (node.alternate) node.alternate = strip_style_elements(node.alternate);
1732
1918
  return node;
1733
1919
  }
1734
1920
 
1735
- if (node.type === 'SwitchStatement') {
1921
+ if (is_switch_control_node(node)) {
1736
1922
  for (const switch_case of node.cases || []) {
1737
1923
  switch_case.consequent = strip_style_elements(switch_case.consequent || []);
1738
1924
  }
1739
1925
  return node;
1740
1926
  }
1741
1927
 
1742
- if (node.type === 'TryStatement') {
1928
+ if (is_try_control_node(node)) {
1743
1929
  node.block = strip_style_elements(node.block);
1744
1930
  if (node.handler?.body) node.handler.body = strip_style_elements(node.handler.body);
1745
1931
  if (node.finalizer) node.finalizer = strip_style_elements(node.finalizer);
@@ -1755,10 +1941,7 @@ function strip_style_elements(node) {
1755
1941
  function is_style_expression_position(path) {
1756
1942
  const parent = path.at(-1);
1757
1943
  return !(
1758
- parent?.type === 'Element' ||
1759
- parent?.type === 'Tsrx' ||
1760
- parent?.type === 'Tsx' ||
1761
- parent?.type === 'TsxCompat' ||
1944
+ is_native_tsrx_node(parent) ||
1762
1945
  parent?.type === 'BlockStatement' ||
1763
1946
  parent?.type === 'Program' ||
1764
1947
  parent?.type === 'SwitchCase'
@@ -1813,9 +1996,11 @@ function create_native_tsrx_statement_list_block(block, transform_context) {
1813
1996
  function create_native_tsrx_render_statements(fragment, transform_context) {
1814
1997
  return with_tsrx_fragment_styles(fragment, transform_context, (style_context) => {
1815
1998
  const target = style_context?.fragment ?? fragment;
1999
+ const render_nodes =
2000
+ target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
1816
2001
  return [
1817
2002
  ...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
1818
- ...build_render_statements(get_tsrx_render_children(target), true, transform_context),
2003
+ ...build_render_statements(render_nodes, true, transform_context),
1819
2004
  ];
1820
2005
  });
1821
2006
  }
@@ -1845,7 +2030,7 @@ function expand_native_tsrx_return_statement_list(statements, transform_context)
1845
2030
  function expand_native_tsrx_return_statement(statement, transform_context) {
1846
2031
  if (!statement || typeof statement !== 'object') return [statement];
1847
2032
 
1848
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'Tsrx') {
2033
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1849
2034
  return create_native_tsrx_render_statements(statement.argument, transform_context);
1850
2035
  }
1851
2036
 
@@ -1858,7 +2043,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1858
2043
  return body === statement.body ? [statement] : [b.block(body, statement)];
1859
2044
  }
1860
2045
 
1861
- if (statement.type === 'IfStatement') {
2046
+ if (is_if_control_node(statement)) {
1862
2047
  const consequent = expand_embedded_native_return_statement(
1863
2048
  statement.consequent,
1864
2049
  transform_context,
@@ -1872,7 +2057,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1872
2057
  return [set_loc(b.if(statement.test, consequent, alternate), statement)];
1873
2058
  }
1874
2059
 
1875
- if (statement.type === 'SwitchStatement') {
2060
+ if (is_switch_control_node(statement)) {
1876
2061
  let changed = false;
1877
2062
  const cases = (statement.cases || []).map((/** @type {any} */ switch_case) => {
1878
2063
  const consequent = expand_native_tsrx_return_statement_list(
@@ -1888,8 +2073,11 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1888
2073
  return changed ? [set_loc(b.switch(statement.discriminant, cases), statement)] : [statement];
1889
2074
  }
1890
2075
 
1891
- if (statement.type === 'TryStatement') {
2076
+ if (is_try_control_node(statement)) {
1892
2077
  const block = expand_embedded_native_return_statement(statement.block, transform_context);
2078
+ const pending = statement.pending
2079
+ ? expand_embedded_native_return_statement(statement.pending, transform_context)
2080
+ : statement.pending;
1893
2081
  const handler_body = statement.handler?.body
1894
2082
  ? expand_embedded_native_return_statement(statement.handler.body, transform_context)
1895
2083
  : statement.handler?.body;
@@ -1898,6 +2086,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1898
2086
  : statement.finalizer;
1899
2087
  if (
1900
2088
  block === statement.block &&
2089
+ pending === statement.pending &&
1901
2090
  handler_body === statement.handler?.body &&
1902
2091
  finalizer === statement.finalizer
1903
2092
  ) {
@@ -1912,7 +2101,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1912
2101
  statement.handler,
1913
2102
  )
1914
2103
  : statement.handler;
1915
- return [set_loc(b.try(block, handler, finalizer, statement.pending ?? null), statement)];
2104
+ return [set_loc(b.try(block, handler, finalizer, pending ?? null), statement)];
1916
2105
  }
1917
2106
 
1918
2107
  return [statement];
@@ -2045,7 +2234,7 @@ function node_contains_hook_bearing_tsrx(node, transform_context) {
2045
2234
  return node.some((child) => node_contains_hook_bearing_tsrx(child, transform_context));
2046
2235
  }
2047
2236
 
2048
- if (node.type === 'Tsrx') {
2237
+ if (is_native_tsrx_node(node)) {
2049
2238
  return body_contains_top_level_hook_call(node.children || [], transform_context, true);
2050
2239
  }
2051
2240
 
@@ -2092,7 +2281,7 @@ function should_extract_hook_helpers(transform_context) {
2092
2281
  */
2093
2282
  function create_module_scoped_hook_component_id(helper_id, transform_context) {
2094
2283
  return create_generated_identifier(
2095
- `${transform_context.helper_state?.base_name || 'Tsrx'}__${helper_id.name}`,
2284
+ `${transform_context.helper_state?.base_name || 'TSRXTemplate'}__${helper_id.name}`,
2096
2285
  );
2097
2286
  }
2098
2287
 
@@ -2310,6 +2499,48 @@ function expand_component_helpers(program) {
2310
2499
  return program;
2311
2500
  }
2312
2501
 
2502
+ /**
2503
+ * Generated helper metadata can be appended after the main transformer walk.
2504
+ * If one of those helpers contains a statement-container body, lower it before
2505
+ * the printer sees the helper subtree.
2506
+ *
2507
+ * @param {any} node
2508
+ * @param {TransformContext} transform_context
2509
+ * @param {Set<any>} [seen]
2510
+ * @returns {void}
2511
+ */
2512
+ function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set()) {
2513
+ if (!node || typeof node !== 'object' || seen.has(node)) return;
2514
+ seen.add(node);
2515
+
2516
+ if (is_function_or_class_boundary(node)) {
2517
+ lower_jsx_code_block_function_body(node);
2518
+ }
2519
+
2520
+ for (const key of Object.keys(node)) {
2521
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
2522
+ let value = node[key];
2523
+ if (!value || typeof value !== 'object') continue;
2524
+
2525
+ if (Array.isArray(value)) {
2526
+ if (key === 'body') {
2527
+ value = node[key] = value.flatMap((child) => {
2528
+ if (child?.type !== 'JSXCodeBlock') return [child];
2529
+ const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
2530
+ return mark_native_pretransformed_jsx(
2531
+ build_render_statements(body_nodes, true, transform_context),
2532
+ );
2533
+ });
2534
+ }
2535
+ for (const child of value) {
2536
+ lower_remaining_jsx_code_blocks(child, transform_context, seen);
2537
+ }
2538
+ } else {
2539
+ lower_remaining_jsx_code_blocks(value, transform_context, seen);
2540
+ }
2541
+ }
2542
+ }
2543
+
2313
2544
  /**
2314
2545
  * Generated helper/statics metadata can be carried on function declarations,
2315
2546
  * variable declarations, object literal members, or export-safe expressions,
@@ -2392,11 +2623,7 @@ function create_component_return_statement(
2392
2623
  * @returns {boolean}
2393
2624
  */
2394
2625
  function is_loop_skip_return_statement(node) {
2395
- return (
2396
- node?.type === 'ReturnStatement' &&
2397
- node.argument == null &&
2398
- node.metadata?.generated_loop_continue_return === true
2399
- );
2626
+ return node?.type === 'ReturnStatement' && node.metadata?.generated_loop_continue_return === true;
2400
2627
  }
2401
2628
 
2402
2629
  /**
@@ -2412,7 +2639,7 @@ function is_loop_skip_if_statement(node) {
2412
2639
  * @returns {any[] | null}
2413
2640
  */
2414
2641
  function get_loop_skip_if_consequent_body(node) {
2415
- if (node?.type !== 'IfStatement' || node.alternate) {
2642
+ if (!is_if_control_node(node) || node.alternate) {
2416
2643
  return null;
2417
2644
  }
2418
2645
 
@@ -2433,7 +2660,15 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
2433
2660
  const branch_statements = build_render_statements(consequent_body, true, transform_context);
2434
2661
  prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
2435
2662
 
2436
- return set_loc(b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null), node);
2663
+ const statement = set_loc(
2664
+ b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null),
2665
+ node,
2666
+ );
2667
+ statement.metadata = {
2668
+ ...(statement.metadata || {}),
2669
+ generated_loop_skip_if: true,
2670
+ };
2671
+ return statement;
2437
2672
  }
2438
2673
 
2439
2674
  /**
@@ -2812,26 +3047,37 @@ function create_helper_props_type_literal_with_typeof_flags(bindings, aliases, u
2812
3047
  /**
2813
3048
  * @param {any} node
2814
3049
  * @param {TransformContext} transform_context
3050
+ * @param {boolean} [in_jsx_child]
2815
3051
  * @returns {any}
2816
3052
  */
2817
- function to_jsx_element(node, transform_context, raw_children = node.children || []) {
2818
- if (node.type === 'JSXElement') return node;
2819
- if (!node.id) {
3053
+ function to_jsx_element(
3054
+ node,
3055
+ transform_context,
3056
+ raw_children = node.children || [],
3057
+ in_jsx_child = false,
3058
+ ) {
3059
+ if (node.type === 'JSXElement' && !node.metadata?.native_tsrx && !is_dynamic_jsx_element(node)) {
3060
+ return node;
3061
+ }
3062
+
3063
+ const source_opening = node.openingElement;
3064
+ const source_name = source_opening?.name;
3065
+ if (!source_name) {
2820
3066
  report_jsx_fragment_in_tsrx_error(node, transform_context);
2821
3067
  return set_loc(b.jsx_fragment(), node);
2822
3068
  }
2823
- if (is_dynamic_element_id(node.id)) {
2824
- return dynamic_element_to_jsx_child(node, transform_context);
3069
+ if (is_dynamic_jsx_element(node)) {
3070
+ return dynamic_element_to_jsx(node, transform_context, in_jsx_child);
2825
3071
  }
2826
3072
 
2827
- const name = identifier_to_jsx_name(node.id);
3073
+ const name = clone_jsx_name(source_name);
2828
3074
  const attributes = transform_element_attributes_dispatch(
2829
- node.attributes || [],
3075
+ source_opening.attributes || [],
2830
3076
  transform_context,
2831
3077
  node,
2832
3078
  );
2833
3079
  const walked_children = node.children || [];
2834
- let selfClosing = !!node.selfClosing;
3080
+ let selfClosing = !!source_opening.selfClosing;
2835
3081
  let children;
2836
3082
  const child_transform = transform_context.platform.hooks?.transformElementChildren?.(
2837
3083
  node,
@@ -2857,7 +3103,7 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
2857
3103
  name,
2858
3104
  attributes,
2859
3105
  selfClosing,
2860
- node.openingElement?.typeArguments,
3106
+ source_opening.typeArguments,
2861
3107
  );
2862
3108
  const openingElement = has_unmappable_attribute
2863
3109
  ? opening_element_node
@@ -3131,7 +3377,13 @@ function collect_block_binding_names_from_statement(statement, names) {
3131
3377
  return;
3132
3378
  }
3133
3379
 
3134
- if (statement.type === 'ForOfStatement' || statement.type === 'ForInStatement') {
3380
+ if (
3381
+ statement.type === 'ForOfStatement' ||
3382
+ statement.type === 'ForInStatement' ||
3383
+ (statement.type === 'JSXForExpression' &&
3384
+ (statement.statementType === 'ForOfStatement' ||
3385
+ statement.statementType === 'ForInStatement'))
3386
+ ) {
3135
3387
  if (statement.left?.type === 'VariableDeclaration' && statement.left.kind === 'var') {
3136
3388
  for (const declaration of statement.left.declarations || []) {
3137
3389
  collect_pattern_names(declaration.id, names);
@@ -3292,7 +3544,7 @@ function validate_hook_outer_assignments_in_node(
3292
3544
  }
3293
3545
  }
3294
3546
 
3295
- if (node.type === 'ForOfStatement') {
3547
+ if (is_for_of_control_node(node)) {
3296
3548
  if (
3297
3549
  node.left &&
3298
3550
  node.left.type !== 'VariableDeclaration' &&
@@ -3725,9 +3977,7 @@ function get_hook_callee_name(callee) {
3725
3977
  * @param {any} source_node
3726
3978
  * @param {TransformContext} transform_context
3727
3979
  * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
3728
- * Used by the switch lift's chained-call build, which allocates ids in
3729
- * source order in a forward pass and then constructs helpers in reverse so
3730
- * each fall-through case can reference the next case's component element.
3980
+ * Used by switch lifting to keep generated helper ids stable in source order.
3731
3981
  * @param {{ transientBindings?: Set<string> }} [options]
3732
3982
  * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
3733
3983
  */
@@ -4006,6 +4256,118 @@ function get_body_source_node(body_nodes) {
4006
4256
  return first;
4007
4257
  }
4008
4258
 
4259
+ /**
4260
+ * @param {any} node
4261
+ * @returns {any}
4262
+ */
4263
+ function jsx_control_expression_to_statement(node) {
4264
+ if (!node?.statementType) return node;
4265
+ return { ...node, type: node.statementType };
4266
+ }
4267
+
4268
+ /**
4269
+ * @param {any} node
4270
+ * @param {TransformContext} transform_context
4271
+ * @returns {any[]}
4272
+ */
4273
+ function get_jsx_code_block_body_nodes(node, transform_context) {
4274
+ if (!node.render) {
4275
+ return node.body || [];
4276
+ }
4277
+
4278
+ if (is_native_tsrx_node(node.render)) {
4279
+ const style_context = prepare_tsrx_fragment_styles(node.render, transform_context);
4280
+ const render = style_context?.fragment ?? node.render;
4281
+ return [
4282
+ ...(node.body || []),
4283
+ ...create_tsrx_style_ref_setup_statements(render, style_context, transform_context),
4284
+ render,
4285
+ ];
4286
+ }
4287
+
4288
+ return [...(node.body || []), node.render];
4289
+ }
4290
+
4291
+ /**
4292
+ * @param {any} node
4293
+ * @returns {any[]}
4294
+ */
4295
+ function get_raw_jsx_code_block_body_nodes(node) {
4296
+ return [...(node.body || []), ...(node.render ? [node.render] : [])];
4297
+ }
4298
+
4299
+ /**
4300
+ * @param {any} node
4301
+ * @returns {boolean}
4302
+ */
4303
+ function is_native_tsrx_node(node) {
4304
+ return (
4305
+ node?.type === 'JSXCodeBlock' ||
4306
+ ((node?.type === 'JSXElement' ||
4307
+ node?.type === 'JSXFragment' ||
4308
+ node?.type === 'JSXStyleElement') &&
4309
+ node.metadata?.native_tsrx)
4310
+ );
4311
+ }
4312
+
4313
+ /**
4314
+ * @param {any} node
4315
+ * @returns {boolean}
4316
+ */
4317
+ function is_dynamic_jsx_element(node) {
4318
+ return !!(
4319
+ node?.type === 'JSXElement' &&
4320
+ (node.dynamic === true ||
4321
+ node.openingElement?.dynamic === true ||
4322
+ is_dynamic_jsx_name(node.openingElement?.name))
4323
+ );
4324
+ }
4325
+
4326
+ /**
4327
+ * @param {any} name
4328
+ * @returns {boolean}
4329
+ */
4330
+ function is_dynamic_jsx_name(name) {
4331
+ if (!name || typeof name !== 'object') return false;
4332
+ if (name.dynamic === true) return true;
4333
+ return name.type === 'JSXMemberExpression' && is_dynamic_jsx_name(name.object);
4334
+ }
4335
+
4336
+ /**
4337
+ * @param {any} node
4338
+ * @returns {boolean}
4339
+ */
4340
+ function is_if_control_node(node) {
4341
+ return node?.type === 'IfStatement' || node?.type === 'JSXIfExpression';
4342
+ }
4343
+
4344
+ /**
4345
+ * @param {any} node
4346
+ * @returns {boolean}
4347
+ */
4348
+ function is_switch_control_node(node) {
4349
+ return node?.type === 'SwitchStatement' || node?.type === 'JSXSwitchExpression';
4350
+ }
4351
+
4352
+ /**
4353
+ * @param {any} node
4354
+ * @returns {boolean}
4355
+ */
4356
+ function is_try_control_node(node) {
4357
+ return node?.type === 'TryStatement' || node?.type === 'JSXTryExpression';
4358
+ }
4359
+
4360
+ /**
4361
+ * @param {any} node
4362
+ * @returns {boolean}
4363
+ */
4364
+ function is_for_of_control_node(node) {
4365
+ return (
4366
+ node?.type === 'ForOfStatement' ||
4367
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForOfStatement')
4368
+ );
4369
+ }
4370
+
4009
4371
  /**
4010
4372
  * @param {any} node
4011
4373
  * @param {TransformContext} transform_context
@@ -4014,37 +4376,56 @@ function get_body_source_node(body_nodes) {
4014
4376
  function to_jsx_child(node, transform_context) {
4015
4377
  if (!node) return node;
4016
4378
  switch (node.type) {
4017
- case 'Tsx':
4018
- // We're inside a JSX child position by construction, so keep a
4019
- // JSXExpressionContainer wrapper for bare `{expr}` children.
4020
- return tsx_node_to_jsx_expression(node, true);
4021
- case 'Tsrx':
4022
- return tsrx_node_to_jsx_expression(node, transform_context, true);
4023
- case 'TsxCompat':
4024
- return tsx_compat_node_to_jsx_expression(node, transform_context, true);
4025
- case 'Element':
4026
- return to_jsx_element(node, transform_context);
4027
- case 'Text':
4028
- return to_jsx_expression_container(to_text_expression(node.expression, node), node);
4029
- case 'TSRXExpression':
4030
- return to_jsx_expression_container(node.expression, node);
4379
+ case 'JSXElement':
4380
+ if (is_native_tsrx_node(node)) {
4381
+ return to_jsx_element(node, transform_context, node.children || [], true);
4382
+ }
4383
+ if (is_dynamic_jsx_element(node)) {
4384
+ return dynamic_element_to_jsx(node, transform_context, true);
4385
+ }
4386
+ return node;
4387
+ case 'JSXFragment':
4388
+ if (is_native_tsrx_node(node)) {
4389
+ return tsrx_node_to_jsx_expression(node, transform_context, true);
4390
+ }
4391
+ return node;
4392
+ case 'JSXIfExpression':
4031
4393
  case 'IfStatement':
4394
+ if (node.metadata?.generated_loop_skip_if) {
4395
+ return node;
4396
+ }
4032
4397
  return (
4033
4398
  transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
4034
- )(node, transform_context);
4399
+ )(jsx_control_expression_to_statement(node), transform_context);
4400
+ case 'JSXForExpression':
4401
+ if (node.statementType !== 'ForOfStatement') {
4402
+ error(
4403
+ 'TSRX `@for` currently supports `for...of` loops in template output.',
4404
+ transform_context.filename,
4405
+ node,
4406
+ transform_context.errors,
4407
+ transform_context.comments,
4408
+ );
4409
+ return to_jsx_expression_container(create_null_literal(), node);
4410
+ }
4411
+ return (
4412
+ transform_context.platform.hooks?.controlFlow?.forOf ?? for_of_statement_to_jsx_child
4413
+ )(jsx_control_expression_to_statement(node), transform_context);
4035
4414
  case 'ForOfStatement':
4036
4415
  return (
4037
4416
  transform_context.platform.hooks?.controlFlow?.forOf ?? for_of_statement_to_jsx_child
4038
4417
  )(node, transform_context);
4418
+ case 'JSXSwitchExpression':
4039
4419
  case 'SwitchStatement':
4040
4420
  return (
4041
4421
  transform_context.platform.hooks?.controlFlow?.switchStatement ??
4042
4422
  switch_statement_to_jsx_child
4043
- )(node, transform_context);
4423
+ )(jsx_control_expression_to_statement(node), transform_context);
4424
+ case 'JSXTryExpression':
4044
4425
  case 'TryStatement':
4045
4426
  return (
4046
4427
  transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
4047
- )(node, transform_context);
4428
+ )(jsx_control_expression_to_statement(node), transform_context);
4048
4429
  default:
4049
4430
  return node;
4050
4431
  }
@@ -4052,8 +4433,8 @@ function to_jsx_child(node, transform_context) {
4052
4433
 
4053
4434
  /**
4054
4435
  * Lower a native TSRX fragment body to a JSX expression.
4055
- * Unlike `<tsx>`, children have already been parsed and transformed through
4056
- * the normal TSRX Element/Text/control-flow visitors.
4436
+ * Children have already been parsed and transformed through the normal TSRX
4437
+ * JSX element/text/control-flow visitors.
4057
4438
  *
4058
4439
  * @param {any} node
4059
4440
  * @param {TransformContext} transform_context
@@ -4136,7 +4517,7 @@ function return_value_statement_to_expression(node, transform_context) {
4136
4517
  return node.argument;
4137
4518
  }
4138
4519
 
4139
- if (node?.type === 'IfStatement') {
4520
+ if (is_if_control_node(node)) {
4140
4521
  return return_value_if_statement_to_conditional_expression(node, transform_context);
4141
4522
  }
4142
4523
 
@@ -4230,7 +4611,7 @@ function return_value_block_to_expression(node, transform_context) {
4230
4611
  * @returns {any | null}
4231
4612
  */
4232
4613
  function return_value_if_statement_to_conditional_expression(node, transform_context) {
4233
- if (!node || node.type !== 'IfStatement') return null;
4614
+ if (!is_if_control_node(node)) return null;
4234
4615
 
4235
4616
  const consequent = return_value_block_to_expression(node.consequent, transform_context);
4236
4617
  if (!consequent) return null;
@@ -4270,14 +4651,14 @@ function if_statement_to_jsx_child(node, transform_context) {
4270
4651
  * @returns {any | null}
4271
4652
  */
4272
4653
  function render_if_statement_to_conditional_expression(node) {
4273
- if (!node || node.type !== 'IfStatement') return null;
4654
+ if (!is_if_control_node(node)) return null;
4274
4655
 
4275
4656
  const consequent = block_statement_to_return_expression(node.consequent);
4276
4657
  if (!consequent) return null;
4277
4658
 
4278
4659
  let alternate = create_null_literal();
4279
4660
  if (node.alternate) {
4280
- if (node.alternate.type === 'IfStatement') {
4661
+ if (is_if_control_node(node.alternate)) {
4281
4662
  alternate = render_if_statement_to_conditional_expression(node.alternate);
4282
4663
  if (!alternate) return null;
4283
4664
  } else {
@@ -4314,25 +4695,11 @@ function block_statement_to_return_expression(block) {
4314
4695
  /**
4315
4696
  * Find the first `key` attribute expression in the top-level elements of a body.
4316
4697
  * Used to propagate keys from loop body elements to wrapper components.
4317
- * Works on both pre-transform (Ripple Element) and post-transform (JSXElement) nodes.
4318
- *
4319
4698
  * @param {any[]} body_nodes
4320
4699
  * @returns {any | undefined}
4321
4700
  */
4322
4701
  function find_key_expression_in_body(body_nodes) {
4323
4702
  for (const node of body_nodes) {
4324
- // Pre-transform: Ripple Element node
4325
- if (node.type === 'Element') {
4326
- for (const attr of node.attributes || []) {
4327
- if (attr.type === 'Attribute') {
4328
- const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
4329
- if (attr_name === 'key') {
4330
- return attr.value?.expression ?? attr.value;
4331
- }
4332
- }
4333
- }
4334
- }
4335
- // Post-transform: JSXElement node
4336
4703
  if (node.type === 'JSXElement') {
4337
4704
  for (const attr of node.openingElement?.attributes || []) {
4338
4705
  if (
@@ -4357,7 +4724,7 @@ function find_key_expression_in_body(body_nodes) {
4357
4724
  * @returns {any}
4358
4725
  */
4359
4726
  function continue_to_bare_return(source_node) {
4360
- const node = set_loc(b.return(null), source_node);
4727
+ const node = set_loc(b.return(create_null_literal()), source_node);
4361
4728
  node.metadata = {
4362
4729
  ...(node.metadata || {}),
4363
4730
  generated_loop_continue_return: true,
@@ -4367,8 +4734,9 @@ function continue_to_bare_return(source_node) {
4367
4734
 
4368
4735
  /**
4369
4736
  * `continue` in a component `for...of` body means "skip this item". JSX targets
4370
- * lower `for...of` to callbacks, so a raw ContinueStatement would be invalid JS;
4371
- * a bare `return` from the callback preserves the item-skip behavior.
4737
+ * lower `for...of` to callbacks, so a raw ContinueStatement would be invalid JS.
4738
+ * Returning null from the callback preserves the item-skip behavior while still
4739
+ * producing an explicit "render nothing" value for JSX runtimes.
4372
4740
  *
4373
4741
  * @param {any[] | any} node
4374
4742
  * @param {boolean} [is_root]
@@ -4376,7 +4744,9 @@ function continue_to_bare_return(source_node) {
4376
4744
  */
4377
4745
  export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4378
4746
  if (Array.isArray(node)) {
4379
- return node.map((child) => rewrite_loop_continues_to_bare_returns(child, false));
4747
+ return node.map((child) =>
4748
+ rewrite_loop_continues_to_bare_returns(child, is_root && !is_loop_statement(child)),
4749
+ );
4380
4750
  }
4381
4751
 
4382
4752
  if (!node || typeof node !== 'object') {
@@ -4401,6 +4771,145 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4401
4771
  return node;
4402
4772
  }
4403
4773
 
4774
+ /**
4775
+ * @param {any[] | any} node
4776
+ * @param {TransformContext} transform_context
4777
+ * @param {boolean} [is_root]
4778
+ */
4779
+ function validate_for_body_control_flow(node, transform_context, is_root = true) {
4780
+ if (Array.isArray(node)) {
4781
+ for (const child of node) {
4782
+ validate_for_body_control_flow(
4783
+ child,
4784
+ transform_context,
4785
+ is_root && !is_loop_statement(child),
4786
+ );
4787
+ }
4788
+ return;
4789
+ }
4790
+
4791
+ if (!node || typeof node !== 'object') {
4792
+ return;
4793
+ }
4794
+
4795
+ if (is_template_if_node(node)) {
4796
+ return;
4797
+ }
4798
+
4799
+ if (node.type === 'ReturnStatement') {
4800
+ error(
4801
+ TSRX_FOR_RETURN_ERROR,
4802
+ transform_context.filename,
4803
+ node,
4804
+ transform_context.errors,
4805
+ transform_context.comments,
4806
+ );
4807
+ return;
4808
+ }
4809
+ if (node.type === 'BreakStatement') {
4810
+ error(
4811
+ TSRX_FOR_BREAK_ERROR,
4812
+ transform_context.filename,
4813
+ node,
4814
+ transform_context.errors,
4815
+ transform_context.comments,
4816
+ );
4817
+ return;
4818
+ }
4819
+ if (node.type === 'ContinueStatement') {
4820
+ error(
4821
+ TSRX_FOR_CONTINUE_ERROR,
4822
+ transform_context.filename,
4823
+ node,
4824
+ transform_context.errors,
4825
+ transform_context.comments,
4826
+ );
4827
+ return;
4828
+ }
4829
+
4830
+ if (is_function_or_class_boundary(node) || (!is_root && is_loop_statement(node))) {
4831
+ return;
4832
+ }
4833
+
4834
+ for (const key of Object.keys(node)) {
4835
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4836
+ continue;
4837
+ }
4838
+ validate_for_body_control_flow(node[key], transform_context, false);
4839
+ }
4840
+ }
4841
+
4842
+ /**
4843
+ * @param {any[] | any} node
4844
+ * @param {TransformContext} transform_context
4845
+ */
4846
+ function validate_if_body_control_flow(node, transform_context) {
4847
+ if (Array.isArray(node)) {
4848
+ for (const child of node) {
4849
+ validate_if_body_control_flow(child, transform_context);
4850
+ }
4851
+ return;
4852
+ }
4853
+
4854
+ if (!node || typeof node !== 'object') {
4855
+ return;
4856
+ }
4857
+
4858
+ if (node.type === 'ReturnStatement') {
4859
+ error(
4860
+ TSRX_IF_RETURN_ERROR,
4861
+ transform_context.filename,
4862
+ node,
4863
+ transform_context.errors,
4864
+ transform_context.comments,
4865
+ );
4866
+ return;
4867
+ }
4868
+ if (node.type === 'BreakStatement') {
4869
+ error(
4870
+ TSRX_IF_BREAK_ERROR,
4871
+ transform_context.filename,
4872
+ node,
4873
+ transform_context.errors,
4874
+ transform_context.comments,
4875
+ );
4876
+ return;
4877
+ }
4878
+ if (node.type === 'ContinueStatement') {
4879
+ error(
4880
+ TSRX_IF_CONTINUE_ERROR,
4881
+ transform_context.filename,
4882
+ node,
4883
+ transform_context.errors,
4884
+ transform_context.comments,
4885
+ );
4886
+ return;
4887
+ }
4888
+
4889
+ if (is_function_or_class_boundary(node)) {
4890
+ return;
4891
+ }
4892
+
4893
+ for (const key of Object.keys(node)) {
4894
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4895
+ continue;
4896
+ }
4897
+ validate_if_body_control_flow(node[key], transform_context);
4898
+ }
4899
+ }
4900
+
4901
+ /**
4902
+ * @param {any} node
4903
+ * @returns {boolean}
4904
+ */
4905
+ function is_template_if_node(node) {
4906
+ return (
4907
+ node?.type === 'JSXIfExpression' ||
4908
+ node?.metadata?.tsrxDirective === 'if' ||
4909
+ (node?.type === 'IfStatement' && node?.statementType === 'IfStatement')
4910
+ );
4911
+ }
4912
+
4404
4913
  /**
4405
4914
  * @param {any} node
4406
4915
  * @returns {boolean}
@@ -4408,8 +4917,11 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4408
4917
  function is_loop_statement(node) {
4409
4918
  return (
4410
4919
  node?.type === 'ForOfStatement' ||
4920
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForOfStatement') ||
4411
4921
  node?.type === 'ForStatement' ||
4922
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForStatement') ||
4412
4923
  node?.type === 'ForInStatement' ||
4924
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForInStatement') ||
4413
4925
  node?.type === 'WhileStatement' ||
4414
4926
  node?.type === 'DoWhileStatement'
4415
4927
  );
@@ -4433,10 +4945,9 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4433
4945
 
4434
4946
  const loop_params = get_for_of_iteration_params(node.left, node.index);
4435
4947
  const loop_body = /** @type {any[]} */ (
4436
- rewrite_loop_continues_to_bare_returns(
4437
- node.body.type === 'BlockStatement' ? node.body.body : [node.body],
4438
- )
4948
+ node.body.type === 'BlockStatement' ? node.body.body : [node.body]
4439
4949
  );
4950
+ validate_for_body_control_flow(loop_body, transform_context);
4440
4951
  const has_hooks =
4441
4952
  should_extract_hook_helpers(transform_context) &&
4442
4953
  body_contains_top_level_hook_call(loop_body, transform_context, true);
@@ -4491,17 +5002,49 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4491
5002
  transform_context.available_bindings = saved_bindings;
4492
5003
 
4493
5004
  const iter_callback = b.arrow(loop_params, b.block(body_statements));
5005
+ const empty_fallback = node.empty
5006
+ ? b.call(
5007
+ b.arrow(
5008
+ [],
5009
+ b.block(
5010
+ build_render_statements(
5011
+ node.empty.type === 'BlockStatement' ? node.empty.body : [node.empty],
5012
+ true,
5013
+ transform_context,
5014
+ ),
5015
+ ),
5016
+ false,
5017
+ undefined,
5018
+ node.empty,
5019
+ ),
5020
+ )
5021
+ : null;
4494
5022
 
4495
5023
  if (transform_context.platform.imports.forOfIterableHelper) {
4496
5024
  transform_context.needs_for_of_iterable = true;
5025
+ const args = [node.right, iter_callback];
5026
+ if (empty_fallback) {
5027
+ args.push(b.literal(null), b.arrow([], empty_fallback));
5028
+ }
5029
+ return to_jsx_expression_container(b.call(b.id(MAP_ITERABLE_INTERNAL_NAME), ...args));
5030
+ }
5031
+
5032
+ const map_call = b.call(b.member(node.right, create_generated_identifier('map')), iter_callback);
5033
+ if (empty_fallback) {
4497
5034
  return to_jsx_expression_container(
4498
- b.call(b.id(MAP_ITERABLE_INTERNAL_NAME), node.right, iter_callback),
5035
+ b.conditional(
5036
+ b.binary(
5037
+ '===',
5038
+ b.member(clone_expression_node(node.right), create_generated_identifier('length')),
5039
+ b.literal(0),
5040
+ ),
5041
+ empty_fallback,
5042
+ map_call,
5043
+ ),
4499
5044
  );
4500
5045
  }
4501
5046
 
4502
- return to_jsx_expression_container(
4503
- b.call(b.member(node.right, create_generated_identifier('map')), iter_callback),
4504
- );
5047
+ return to_jsx_expression_container(map_call);
4505
5048
  }
4506
5049
 
4507
5050
  /**
@@ -4511,25 +5054,6 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4511
5054
  */
4512
5055
  function apply_key_to_loop_body(body_nodes, key_expression) {
4513
5056
  for (const node of body_nodes) {
4514
- if (node.type === 'Element') {
4515
- const attributes = node.attributes || (node.attributes = []);
4516
- const has_key = attributes.some((/** @type {any} */ attr) => {
4517
- const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
4518
- return attr_name === 'key';
4519
- });
4520
-
4521
- if (!has_key) {
4522
- attributes.push({
4523
- type: 'Attribute',
4524
- name: b.id('key'),
4525
- value: clone_expression_node(key_expression),
4526
- shorthand: false,
4527
- metadata: { path: [] },
4528
- });
4529
- }
4530
- return;
4531
- }
4532
-
4533
5057
  if (node.type === 'JSXElement') {
4534
5058
  const attributes = node.openingElement?.attributes || [];
4535
5059
  const has_key = attributes.some(
@@ -4559,7 +5083,7 @@ function apply_key_to_loop_body(body_nodes, key_expression) {
4559
5083
  function should_apply_key_to_loop_body(body_nodes) {
4560
5084
  let keyable_children = 0;
4561
5085
  for (const node of body_nodes) {
4562
- if (node.type === 'Element' || node.type === 'JSXElement') {
5086
+ if (node.type === 'JSXElement') {
4563
5087
  keyable_children += 1;
4564
5088
  }
4565
5089
  }
@@ -4649,11 +5173,11 @@ function switch_statement_to_jsx_child(node, transform_context) {
4649
5173
  }
4650
5174
 
4651
5175
  /**
4652
- * Transform a `try { ... } pending { ... } catch (err, reset) { ... }` block
5176
+ * Transform an `@try { ... } @pending { ... } @catch (err, reset) { ... }` block
4653
5177
  * into React `<TsrxErrorBoundary>` and/or `<Suspense>` JSX elements.
4654
5178
  *
4655
- * - `pending` → `<Suspense fallback={...}>`
4656
- * - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
5179
+ * - `@pending` → `<Suspense fallback={...}>`
5180
+ * - `@catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
4657
5181
  * - both → ErrorBoundary wraps Suspense
4658
5182
  * - JavaScript `try/finally` is not part of component template control flow
4659
5183
  *
@@ -4697,30 +5221,6 @@ function try_statement_to_jsx_child(node, transform_context) {
4697
5221
  );
4698
5222
  }
4699
5223
 
4700
- // Validate that try body contains JSX if pending block is present
4701
- if (pending) {
4702
- const try_body = node.block.body || [];
4703
- if (!try_body.some(is_jsx_child)) {
4704
- error(
4705
- 'TSRX try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
4706
- transform_context.filename,
4707
- node.block,
4708
- transform_context.errors,
4709
- transform_context.comments,
4710
- );
4711
- }
4712
- const pending_body = pending.body || [];
4713
- if (pending_body.length > 0 && !pending_body.some(is_jsx_child)) {
4714
- error(
4715
- 'TSRX try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
4716
- transform_context.filename,
4717
- pending,
4718
- transform_context.errors,
4719
- transform_context.comments,
4720
- );
4721
- }
4722
- }
4723
-
4724
5224
  // Build the try body content as JSX children
4725
5225
  const try_body_nodes = node.block.body || [];
4726
5226
  const try_content = statement_body_to_jsx_child(try_body_nodes, transform_context);
@@ -4873,8 +5373,8 @@ function create_jsx_element(tag_name, attributes, children) {
4873
5373
 
4874
5374
  /**
4875
5375
  * Inject runtime-helper import declarations the transform decided it needed
4876
- * during the walk: `Suspense` for `try { ... } pending { ... }`,
4877
- * `TsrxErrorBoundary` for `try { ... } catch (...)`, and `mergeRefs` for
5376
+ * during the walk: `Suspense` for `@try { ... } @pending { ... }`,
5377
+ * `TsrxErrorBoundary` for `@try { ... } @catch (...)`, and `mergeRefs` for
4878
5378
  * elements with multiple `ref` attributes under the `'merge-refs'`
4879
5379
  * strategy. Import sources are platform-specific.
4880
5380
  *
@@ -4988,16 +5488,22 @@ function add_ref_import_specifier(imports, source, specifier) {
4988
5488
  function create_render_if_statement(node, transform_context) {
4989
5489
  const consequent_body =
4990
5490
  node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
5491
+ if (is_template_if_node(node)) {
5492
+ validate_if_body_control_flow(consequent_body, transform_context);
5493
+ }
4991
5494
  const consequent_has_hooks =
4992
5495
  should_extract_hook_helpers(transform_context) &&
4993
5496
  body_contains_top_level_hook_call(consequent_body, transform_context, true);
4994
5497
 
4995
5498
  let alternate = null;
4996
5499
  if (node.alternate) {
4997
- if (node.alternate.type === 'IfStatement') {
5500
+ if (is_if_control_node(node.alternate)) {
4998
5501
  alternate = create_render_if_statement(node.alternate, transform_context);
4999
5502
  } else {
5000
5503
  const alternate_body = node.alternate.body || [node.alternate];
5504
+ if (is_template_if_node(node)) {
5505
+ validate_if_body_control_flow(alternate_body, transform_context);
5506
+ }
5001
5507
  const alternate_has_hooks =
5002
5508
  should_extract_hook_helpers(transform_context) &&
5003
5509
  body_contains_top_level_hook_call(alternate_body, transform_context, true);
@@ -5034,9 +5540,8 @@ function create_render_if_statement(node, transform_context) {
5034
5540
  * case body needs to be hoisted into its own helper component or can stay
5035
5541
  * inline.
5036
5542
  *
5037
- * `own_body` is everything in the case's `consequent` up to (and including for
5038
- * `return <expr>`, excluding for `break` / bare `return;`) the first
5039
- * terminator. `has_terminator` records whether such a terminator was seen.
5543
+ * `own_body` is the case's isolated consequent. JSX `@switch` cases do not
5544
+ * fall through, so `break` is not part of the template switch model.
5040
5545
  *
5041
5546
  * @param {any[]} consequent
5042
5547
  * @returns {{ own_body: any[], has_terminator: boolean }}
@@ -5045,10 +5550,6 @@ function summarize_switch_case_body(consequent) {
5045
5550
  const own_body = [];
5046
5551
  let has_terminator = false;
5047
5552
  for (const child of consequent) {
5048
- if (child.type === 'BreakStatement') {
5049
- has_terminator = true;
5050
- break;
5051
- }
5052
5553
  if (child.type === 'ReturnStatement' && child.argument == null) {
5053
5554
  has_terminator = true;
5054
5555
  break;
@@ -5081,11 +5582,10 @@ export function clone_switch_helper_invocation(helper) {
5081
5582
 
5082
5583
  /**
5083
5584
  * Plan the switch lift: decide which case bodies to hoist into their own
5084
- * helper components, build them in reverse so each helper can chain into the
5085
- * next, and return everything callers need to construct a target-specific
5086
- * switch shape (a JS `switch` for React/Preact/Vue or `<Switch>/<Match>` for
5087
- * Solid). Centralizes the lift bookkeeping so both consumers see the same
5088
- * hook-detection rules, duplication analysis, and helper-id numbering.
5585
+ * helper components and return everything callers need to construct a
5586
+ * target-specific switch shape (a JS `switch` for React/Preact/Vue or
5587
+ * `<Switch>/<Match>` for Solid). JSX `@switch` cases are isolated and do not
5588
+ * fall through.
5089
5589
  *
5090
5590
  * Returned helpers — when non-null — are already constructed via
5091
5591
  * `create_hook_safe_helper`, which is the same path hook-bearing case bodies
@@ -5099,7 +5599,6 @@ export function clone_switch_helper_invocation(helper) {
5099
5599
  * @returns {{
5100
5600
  * case_info: Array<{ own_body: any[], has_terminator: boolean }>,
5101
5601
  * case_helpers: Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>,
5102
- * find_next_helper_after: (from_index: number) => { component_element: ESTreeJSX.JSXElement } | null,
5103
5602
  * setup_statements: any[],
5104
5603
  * }}
5105
5604
  */
@@ -5109,22 +5608,15 @@ export function plan_switch_lift(switch_node, transform_context) {
5109
5608
  return summarize_switch_case_body(consequent);
5110
5609
  });
5111
5610
 
5112
- // A case body needs to be lifted iff (a) it would render in more than one
5113
- // arm after fall-through expansion, or (b) it contains hooks (which always
5114
- // went through the lift pipeline before this change). Duplication happens
5115
- // exactly when the previous case has no terminator — that's the only way
5116
- // an earlier arm can reach this body via JS fall-through semantics.
5611
+ // A case body needs to be lifted iff it contains hooks. Cases are isolated,
5612
+ // so downstream case bodies are never duplicated into earlier arms.
5117
5613
  const needs_helper = case_info.map(
5118
- (/** @type {{ own_body: any[], has_terminator: boolean }} */ info, /** @type {number} */ k) => {
5614
+ (/** @type {{ own_body: any[], has_terminator: boolean }} */ info) => {
5119
5615
  if (info.own_body.length === 0) return false;
5120
- if (
5616
+ return (
5121
5617
  should_extract_hook_helpers(transform_context) &&
5122
5618
  body_contains_top_level_hook_call(info.own_body, transform_context, true)
5123
- ) {
5124
- return true;
5125
- }
5126
- if (k === 0) return false;
5127
- return !case_info[k - 1].has_terminator;
5619
+ );
5128
5620
  },
5129
5621
  );
5130
5622
 
@@ -5141,37 +5633,12 @@ export function plan_switch_lift(switch_node, transform_context) {
5141
5633
  /** @type {Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>} */
5142
5634
  const case_helpers = new Array(switch_node.cases.length).fill(null);
5143
5635
 
5144
- /**
5145
- * Find the next downstream helper this arm chains into when it has no
5146
- * terminator: scan forward past any empty cases until we hit either a
5147
- * helper-bearing case or a case whose body has a terminator (which stops
5148
- * the chain — JS would have `break`/`return`ed out at that point).
5149
- *
5150
- * @param {number} from_index
5151
- * @returns {{ component_element: ESTreeJSX.JSXElement } | null}
5152
- */
5153
- function find_next_helper_after(from_index) {
5154
- for (let j = from_index + 1; j < switch_node.cases.length; j++) {
5155
- if (case_helpers[j]) return case_helpers[j];
5156
- if (case_info[j].has_terminator) return null;
5157
- }
5158
- return null;
5159
- }
5160
-
5161
5636
  for (let i = switch_node.cases.length - 1; i >= 0; i--) {
5162
5637
  if (!needs_helper[i]) continue;
5163
- const { own_body, has_terminator } = case_info[i];
5164
-
5165
- let helper_body = own_body;
5166
- if (!has_terminator) {
5167
- const next_helper = find_next_helper_after(i);
5168
- if (next_helper) {
5169
- helper_body = [...own_body, clone_switch_helper_invocation(next_helper)];
5170
- }
5171
- }
5638
+ const { own_body } = case_info[i];
5172
5639
 
5173
5640
  case_helpers[i] = create_hook_safe_helper(
5174
- helper_body,
5641
+ own_body,
5175
5642
  undefined,
5176
5643
  switch_node.cases[i],
5177
5644
  transform_context,
@@ -5189,40 +5656,17 @@ export function plan_switch_lift(switch_node, transform_context) {
5189
5656
  return {
5190
5657
  case_info,
5191
5658
  case_helpers,
5192
- find_next_helper_after,
5193
5659
  setup_statements,
5194
5660
  };
5195
5661
  }
5196
5662
 
5197
5663
  /**
5198
- * Switch lift for fall-through deduplication. Reuses the same `create_hook_safe_helper`
5199
- * pipeline as hook-bearing case bodies: every case whose body would otherwise
5200
- * appear in 2+ arms (because the previous case had no `break` / `return`) is
5201
- * hoisted into its own helper component, and each upstream arm references the
5202
- * next helper at the end of its own body to materialize JS fall-through at
5203
- * render time. Cases whose bodies live in exactly one arm stay inline so the
5204
- * common (break-terminated) shape compiles to the same simple switch as before
5205
- * the lift was introduced.
5206
- *
5207
- * The chain pattern:
5208
- * helper_idle = () => <><Online/><Helper_active/></>
5209
- * helper_active = () => <><Away/><Helper_offline/></>
5210
- * helper_offline = () => <Offline/>
5211
- *
5212
- * case "idle": return <Helper_idle/>
5213
- * case "active": return <Helper_active/>
5214
- * case "offline": return <Helper_offline/>
5215
- *
5216
- * Each case body appears exactly once in the generated module — matching how
5217
- * we already handle hook-bearing case bodies — which keeps the bundle from
5218
- * growing quadratically in case count and means editor mappings are 1:1.
5219
- *
5220
5664
  * @param {any} switch_node
5221
5665
  * @param {TransformContext} transform_context
5222
5666
  * @returns {{ setup_statements: any[], switch_statement: any }}
5223
5667
  */
5224
5668
  function build_switch_with_lift(switch_node, transform_context) {
5225
- const { case_info, case_helpers, find_next_helper_after, setup_statements } = plan_switch_lift(
5669
+ const { case_info, case_helpers, setup_statements } = plan_switch_lift(
5226
5670
  switch_node,
5227
5671
  transform_context,
5228
5672
  );
@@ -5242,10 +5686,10 @@ function build_switch_with_lift(switch_node, transform_context) {
5242
5686
  const { own_body, has_terminator } = case_info[i];
5243
5687
 
5244
5688
  if (own_body.length === 0 && !has_terminator) {
5245
- // Alias-pattern empty case (`case 'a': case 'b': ...`) — keep
5246
- // the arm body empty so JS falls through to the next case at
5247
- // runtime, where the helper invocation actually lives.
5248
- return set_loc(b.switch_case(original_case.test, []), original_case);
5689
+ return set_loc(
5690
+ b.switch_case(original_case.test, [create_null_return_statement()]),
5691
+ original_case,
5692
+ );
5249
5693
  }
5250
5694
 
5251
5695
  const case_body = [];
@@ -5272,27 +5716,13 @@ function build_switch_with_lift(switch_node, transform_context) {
5272
5716
  }
5273
5717
  }
5274
5718
 
5275
- if (!has_terminal && !has_terminator) {
5276
- const next_helper = find_next_helper_after(i);
5277
- if (next_helper) {
5278
- render_nodes.push(clone_switch_helper_invocation(next_helper));
5279
- }
5280
- }
5281
-
5282
5719
  if (!has_terminal) {
5283
5720
  if (render_nodes.length > 0) {
5284
5721
  case_body.push(create_component_return_statement(render_nodes, original_case));
5285
- } else if (has_terminator) {
5286
- // Empty body with explicit `break;` / bare `return;` — keep
5287
- // a `break` so JS doesn't fall through into the next case
5288
- // (which may now hold the lifted helper invocation).
5289
- case_body.push(b.break);
5290
5722
  } else if (case_body.length > 0) {
5291
- // Statements-only inline case without terminator. We've
5292
- // already inlined the downstream chain via the helper
5293
- // reference above, so emit a `break` to stop the runtime
5294
- // from re-running downstream statements via JS fall-through.
5295
- case_body.push(b.break);
5723
+ case_body.push(create_null_return_statement());
5724
+ } else if (has_terminator) {
5725
+ case_body.push(create_null_return_statement());
5296
5726
  }
5297
5727
  }
5298
5728
 
@@ -5369,12 +5799,12 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
5369
5799
  * @returns {boolean}
5370
5800
  */
5371
5801
  export function is_component_like_element(element) {
5372
- const id = element?.id;
5373
- if (!id) return false;
5374
- if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
5375
- if (id.type === 'JSXIdentifier') return /^[A-Z]/.test(id.name);
5376
- if (id.type === 'MemberExpression') return true;
5377
- if (id.type === 'JSXMemberExpression') return true;
5802
+ const name = element?.openingElement?.name;
5803
+ if (!name) return false;
5804
+ if (name.type === 'Identifier') return /^[A-Z]/.test(name.name);
5805
+ if (name.type === 'JSXIdentifier') return /^[A-Z]/.test(name.name);
5806
+ if (name.type === 'MemberExpression') return true;
5807
+ if (name.type === 'JSXMemberExpression') return true;
5378
5808
  return false;
5379
5809
  }
5380
5810
 
@@ -5527,10 +5957,7 @@ function wrap_jsx_setup_declarations(expression, in_jsx_child) {
5527
5957
  /**
5528
5958
  * Reject elements with more than one TSX-style `ref={...}` attribute.
5529
5959
  * This validator runs over the raw, pre-lowering attribute list so each
5530
- * shape is still distinguishable by `type`. Ripple `Element` attributes have type `Attribute` with an
5531
- * `Identifier` name (the parser normalizes `JSXAttribute`/`JSXIdentifier`
5532
- * for non-Tsx elements); inside `<tsx:react>` compat blocks they retain
5533
- * the original `JSXAttribute`/`JSXIdentifier` shape, so we accept both.
5960
+ * shape is still distinguishable by `type`.
5534
5961
  *
5535
5962
  * @param {any[]} raw_attrs
5536
5963
  * @param {TransformContext} [transform_context]
@@ -5541,14 +5968,10 @@ export function validate_at_most_one_ref_attribute(raw_attrs, transform_context)
5541
5968
  for (const attr of raw_attrs) {
5542
5969
  if (!attr) continue;
5543
5970
  const is_ref_attr =
5544
- (attr.type === 'Attribute' &&
5545
- attr.name &&
5546
- attr.name.type === 'Identifier' &&
5547
- attr.name.name === 'ref') ||
5548
- (attr.type === 'JSXAttribute' &&
5549
- attr.name &&
5550
- attr.name.type === 'JSXIdentifier' &&
5551
- attr.name.name === 'ref');
5971
+ attr.type === 'JSXAttribute' &&
5972
+ attr.name &&
5973
+ attr.name.type === 'JSXIdentifier' &&
5974
+ attr.name.name === 'ref';
5552
5975
  if (!is_ref_attr) continue;
5553
5976
  refs.push(attr.name);
5554
5977
  }
@@ -5760,8 +6183,6 @@ function infer_ref_namespace(tag_name) {
5760
6183
  * @returns {string | null}
5761
6184
  */
5762
6185
  function get_element_ref_tag_name(element) {
5763
- const id = element?.id;
5764
- if (id?.type === 'Identifier') return id.name;
5765
6186
  const name = element?.name;
5766
6187
  if (name?.type === 'JSXIdentifier') return name.name;
5767
6188
  if (element?.openingElement?.name?.type === 'JSXIdentifier') {
@@ -5797,15 +6218,6 @@ export function to_jsx_attribute(attr, transform_context) {
5797
6218
  if (attr.type === 'JSXSpreadAttribute') {
5798
6219
  return attr;
5799
6220
  }
5800
- if (attr.type === 'SpreadAttribute') {
5801
- return set_loc(
5802
- /** @type {any} */ ({
5803
- type: 'JSXSpreadAttribute',
5804
- argument: attr.argument,
5805
- }),
5806
- attr,
5807
- );
5808
- }
5809
6221
  // Keep this legacy hook for targets that need React-style DOM attrs. The
5810
6222
  // current first-party targets preserve authored `class`.
5811
6223
  let attr_name = attr.name;
@@ -5857,25 +6269,29 @@ function value_has_unmappable_jsx_loc(value) {
5857
6269
  /**
5858
6270
  * @param {any} node
5859
6271
  * @param {TransformContext} transform_context
5860
- * @returns {ESTreeJSX.JSXExpressionContainer}
6272
+ * @param {boolean} in_jsx_child
6273
+ * @returns {any}
5861
6274
  */
5862
- function dynamic_element_to_jsx_child(node, transform_context) {
5863
- const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
5864
- const alias_declaration = set_loc(b.const(dynamic_id, clone_expression_node(node.id)), node);
6275
+ function dynamic_element_to_jsx(node, transform_context, in_jsx_child) {
6276
+ const source_name = node.openingElement?.name;
6277
+ const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), source_name || node);
6278
+ const alias_declaration = set_loc(
6279
+ b.const(dynamic_id, jsx_name_to_expression(source_name)),
6280
+ source_name || node,
6281
+ );
5865
6282
  const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
5866
6283
 
5867
- return to_jsx_expression_container(
5868
- b.call(
5869
- b.arrow(
5870
- [],
5871
- b.block([
5872
- alias_declaration,
5873
- b.return(b.conditional(clone_identifier(dynamic_id), jsx_element, create_null_literal())),
5874
- ]),
5875
- ),
6284
+ const expression = b.call(
6285
+ b.arrow(
6286
+ [],
6287
+ b.block([
6288
+ alias_declaration,
6289
+ b.return(b.conditional(clone_identifier(dynamic_id), jsx_element, create_null_literal())),
6290
+ ]),
5876
6291
  ),
5877
- node,
5878
6292
  );
6293
+
6294
+ return in_jsx_child ? to_jsx_expression_container(expression, node) : set_loc(expression, node);
5879
6295
  }
5880
6296
 
5881
6297
  /**
@@ -5886,11 +6302,11 @@ function dynamic_element_to_jsx_child(node, transform_context) {
5886
6302
  */
5887
6303
  function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
5888
6304
  const attributes = transform_element_attributes_dispatch(
5889
- node.attributes || [],
6305
+ node.openingElement?.attributes || [],
5890
6306
  transform_context,
5891
6307
  node,
5892
6308
  );
5893
- const selfClosing = !!node.selfClosing;
6309
+ const selfClosing = !!node.openingElement?.selfClosing;
5894
6310
  const children = create_element_children(node.children || [], transform_context);
5895
6311
  const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
5896
6312
 
@@ -5912,6 +6328,10 @@ function build_return_expression(render_nodes) {
5912
6328
  if (only.type === 'JSXExpressionContainer') {
5913
6329
  return only.expression;
5914
6330
  }
6331
+ if (only.type === 'JSXText') {
6332
+ const value = (only.value ?? '').trim();
6333
+ return b.literal(value, JSON.stringify(value), only);
6334
+ }
5915
6335
  return only;
5916
6336
  }
5917
6337
  const first = render_nodes[0];
@@ -5930,25 +6350,3 @@ function build_return_expression(render_nodes) {
5930
6350
  : undefined,
5931
6351
  );
5932
6352
  }
5933
-
5934
- /**
5935
- * @param {any} node
5936
- * @param {TransformContext} transform_context
5937
- * @param {boolean} [in_jsx_child]
5938
- * @returns {any}
5939
- */
5940
- function tsx_compat_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
5941
- const platform = transform_context.platform;
5942
- if (!platform.jsx.acceptedTsxKinds.includes(node.kind)) {
5943
- const accepted = platform.jsx.acceptedTsxKinds.map((k) => `<tsx:${k}>`).join(', ');
5944
- error(
5945
- `${platform.name} TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or one of: ${accepted}.`,
5946
- transform_context.filename,
5947
- node,
5948
- transform_context.errors,
5949
- transform_context.comments,
5950
- );
5951
- }
5952
-
5953
- return tsx_node_to_jsx_expression(node, in_jsx_child);
5954
- }