@tsrx/core 0.1.20 → 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';
@@ -63,6 +63,17 @@ 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
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,7 +296,11 @@ export function createJsxTransform(platform) {
213
296
  return next();
214
297
  },
215
298
 
216
- TsrxFragment(node, { next, path, state, visit }) {
299
+ JSXFragment(node, { next, path, state, visit }) {
300
+ if (!node.metadata?.native_tsrx) {
301
+ return next() ?? node;
302
+ }
303
+
217
304
  const parent = /** @type {AST.ArrowFunctionExpression} */ (path.at(-1));
218
305
  if (parent?.metadata?.native_tsrx && parent.body === node) {
219
306
  return /** @type {any} */ (visit(create_native_tsrx_render_block(node, state), state));
@@ -233,18 +320,15 @@ export function createJsxTransform(platform) {
233
320
  return /** @type {any} */ (wrap_jsx_setup_declarations(expression, in_jsx_child));
234
321
  },
235
322
 
236
- TsxCompat(node, { next, path, state }) {
237
- const inner = /** @type {any} */ (next() ?? node);
238
- const in_jsx_child = in_jsx_child_context(path);
239
- return /** @type {any} */ (
240
- wrap_jsx_setup_declarations(
241
- tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child),
242
- in_jsx_child,
243
- )
244
- );
245
- },
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
+ }
246
331
 
247
- Element(node, { next, path, state }) {
248
332
  if (is_style_element(node) && is_style_expression_position(path)) {
249
333
  const stylesheet = get_style_element_stylesheet(node);
250
334
  if (stylesheet) {
@@ -254,30 +338,57 @@ export function createJsxTransform(platform) {
254
338
  }
255
339
  }
256
340
 
257
- // Capture raw children BEFORE the walker transforms them so a
258
- // platform hook (e.g. Solid's textContent optimization) can
259
- // inspect the original Text / TSRXExpression nodes rather than
260
- // 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.
261
343
  const raw_children = /** @type {any} */ (node.children || []).map(
262
344
  (/** @type {any} */ child) => (child && typeof child === 'object' ? { ...child } : child),
263
345
  );
264
346
  const inner = /** @type {any} */ (next() ?? node);
265
347
  const hook = platform.hooks?.transformElement;
266
348
  if (hook) return /** @type {any} */ (hook(inner, state, raw_children));
267
- 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
+ );
268
352
  },
269
353
 
270
- Text(node, { next }) {
271
- 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
+ }
272
382
  return /** @type {any} */ (
273
- 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
+ )
274
388
  );
275
389
  },
276
390
 
277
- TSRXExpression(node, { next }) {
278
- const inner = /** @type {any} */ (next() ?? node);
279
- return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
280
- },
391
+ JSXCodeBlock: transform_jsx_code_block,
281
392
 
282
393
  BlockStatement: transform_block_statement,
283
394
  ReturnStatement: transform_return_statement,
@@ -330,6 +441,7 @@ export function createJsxTransform(platform) {
330
441
  ? expanded
331
442
  : apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
332
443
  );
444
+ lower_remaining_jsx_code_blocks(final_program, transform_context);
333
445
 
334
446
  const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
335
447
  sourceMapSource: filename,
@@ -408,7 +520,7 @@ function collect_css_prunable_elements(value, elements = []) {
408
520
  return elements;
409
521
  }
410
522
 
411
- if (value.type === 'Element') {
523
+ if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
412
524
  if (!is_style_element(value)) {
413
525
  elements.push(value);
414
526
  }
@@ -440,6 +552,12 @@ function build_component_statements(body_nodes, transform_context) {
440
552
  * @returns {any[]}
441
553
  */
442
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
+
443
561
  const statements = [];
444
562
  const render_nodes = [];
445
563
  let has_terminal_return = false;
@@ -522,7 +640,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
522
640
  }
523
641
 
524
642
  if (
525
- child.type === 'ForOfStatement' &&
643
+ is_for_of_control_node(child) &&
526
644
  !child.await &&
527
645
  should_extract_hook_helpers(transform_context) &&
528
646
  !transform_context.platform.hooks?.isTopLevelSetupCall &&
@@ -533,7 +651,10 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
533
651
  true,
534
652
  )
535
653
  ) {
536
- 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
+ );
537
658
  if (hoisted) {
538
659
  statements.push(...hoisted.hoist_statements);
539
660
  if (interleaved && is_capturable_jsx_child(hoisted.jsx_child)) {
@@ -560,6 +681,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
560
681
  } else if (is_bare_render_expression(child)) {
561
682
  render_nodes.push(to_jsx_expression_container(child, child));
562
683
  } else {
684
+ mark_nested_function_return_jsx(child);
563
685
  statements.push(child);
564
686
  collect_statement_bindings(child, transform_context.available_bindings);
565
687
  }
@@ -667,7 +789,7 @@ function find_hook_split_index(body_nodes, transform_context) {
667
789
  * @returns {boolean}
668
790
  */
669
791
  function is_component_body_conditional_return_statement(node) {
670
- if (node?.type !== 'IfStatement') {
792
+ if (!is_if_control_node(node)) {
671
793
  return false;
672
794
  }
673
795
 
@@ -702,20 +824,20 @@ function statement_contains_component_body_return(node) {
702
824
  return (node.body || []).some(statement_contains_component_body_return);
703
825
  }
704
826
 
705
- if (node.type === 'IfStatement') {
827
+ if (is_if_control_node(node)) {
706
828
  return (
707
829
  statement_contains_component_body_return(node.consequent) ||
708
830
  statement_contains_component_body_return(node.alternate)
709
831
  );
710
832
  }
711
833
 
712
- if (node.type === 'SwitchStatement') {
834
+ if (is_switch_control_node(node)) {
713
835
  return (node.cases || []).some((/** @type {any} */ switch_case) =>
714
836
  statement_contains_component_body_return(switch_case.consequent || []),
715
837
  );
716
838
  }
717
839
 
718
- if (node.type === 'TryStatement') {
840
+ if (is_try_control_node(node)) {
719
841
  return (
720
842
  statement_contains_component_body_return(node.block) ||
721
843
  statement_contains_component_body_return(node.handler?.body) ||
@@ -1008,13 +1130,41 @@ function transform_block_statement(node, { next, visit, state, path }) {
1008
1130
  * @returns {any}
1009
1131
  */
1010
1132
  function transform_return_statement(node, { next, visit, state, path }) {
1011
- if (get_active_native_tsrx_function(path) && node.argument?.type === 'TsrxFragment') {
1133
+ if (get_active_native_tsrx_function(path) && is_native_tsrx_node(node.argument)) {
1012
1134
  return visit(create_native_tsrx_render_block(node.argument, state), state);
1013
1135
  }
1014
1136
 
1015
1137
  return next() ?? node;
1016
1138
  }
1017
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
+
1018
1168
  /**
1019
1169
  * @param {AST.Node[]} path
1020
1170
  * @returns {any | null}
@@ -1035,6 +1185,13 @@ function get_active_native_tsrx_function(path) {
1035
1185
  * @returns {any}
1036
1186
  */
1037
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
+
1038
1195
  if (node.metadata?.native_tsrx_function || function_has_native_tsrx_return(node)) {
1039
1196
  return transform_native_tsrx_function(node, context);
1040
1197
  }
@@ -1042,6 +1199,34 @@ function transform_function(node, context) {
1042
1199
  return transform_function_with_hook_helpers(node, context);
1043
1200
  }
1044
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
+
1045
1230
  /**
1046
1231
  * @param {any} node
1047
1232
  * @param {{ next: () => any, state: TransformContext }} context
@@ -1072,7 +1257,7 @@ function transform_native_tsrx_function(node, { next, state }) {
1072
1257
  if (
1073
1258
  inner !== node &&
1074
1259
  node.type === 'ArrowFunctionExpression' &&
1075
- node.body?.type === 'TsrxFragment' &&
1260
+ is_native_tsrx_node(node.body) &&
1076
1261
  inner.body?.type === 'BlockStatement'
1077
1262
  ) {
1078
1263
  inner.expression = false;
@@ -1132,6 +1317,10 @@ function find_native_await(node) {
1132
1317
  return find_first_top_level_await(node.body, false);
1133
1318
  }
1134
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
+
1135
1324
  const body = node.body?.type === 'BlockStatement' ? node.body.body || [] : [];
1136
1325
  return find_native_await_in_list(body);
1137
1326
  }
@@ -1155,7 +1344,7 @@ function find_native_await_in_list(statements) {
1155
1344
  function find_native_await_in_statement(statement) {
1156
1345
  if (!statement || typeof statement !== 'object') return null;
1157
1346
 
1158
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'TsrxFragment') {
1347
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1159
1348
  return find_first_top_level_await_in_tsrx_function_body(statement.argument.children || []);
1160
1349
  }
1161
1350
 
@@ -1174,14 +1363,14 @@ function find_native_await_in_statement(statement) {
1174
1363
  return find_native_await_in_list(statement.body || []);
1175
1364
  }
1176
1365
 
1177
- if (statement.type === 'IfStatement') {
1366
+ if (is_if_control_node(statement)) {
1178
1367
  return (
1179
1368
  find_native_await_in_statement(statement.consequent) ||
1180
1369
  find_native_await_in_statement(statement.alternate)
1181
1370
  );
1182
1371
  }
1183
1372
 
1184
- if (statement.type === 'SwitchStatement') {
1373
+ if (is_switch_control_node(statement)) {
1185
1374
  for (const switch_case of statement.cases || []) {
1186
1375
  const found = find_native_await_in_list(switch_case.consequent || []);
1187
1376
  if (found) return found;
@@ -1189,7 +1378,7 @@ function find_native_await_in_statement(statement) {
1189
1378
  return null;
1190
1379
  }
1191
1380
 
1192
- if (statement.type === 'TryStatement') {
1381
+ if (is_try_control_node(statement)) {
1193
1382
  return (
1194
1383
  find_native_await_in_statement(statement.block) ||
1195
1384
  find_native_await_in_statement(statement.handler?.body) ||
@@ -1197,7 +1386,7 @@ function find_native_await_in_statement(statement) {
1197
1386
  );
1198
1387
  }
1199
1388
 
1200
- return null;
1389
+ return find_first_top_level_await(statement, false);
1201
1390
  }
1202
1391
 
1203
1392
  /**
@@ -1250,7 +1439,7 @@ function transform_function_with_hook_helpers(node, { next, state }) {
1250
1439
  * @returns {string}
1251
1440
  */
1252
1441
  function get_function_helper_base_name(node) {
1253
- return get_function_like_name(node) || 'TsrxFragment';
1442
+ return get_function_like_name(node) || 'TSRXTemplate';
1254
1443
  }
1255
1444
 
1256
1445
  /**
@@ -1329,7 +1518,7 @@ function collect_function_scope_bindings(node) {
1329
1518
  const bindings = collect_param_bindings(node.params || []);
1330
1519
  if (node.body?.type === 'BlockStatement') {
1331
1520
  for (const statement of node.body.body || []) {
1332
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'TsrxFragment') {
1521
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1333
1522
  for (const child of get_tsrx_render_children(statement.argument)) {
1334
1523
  collect_statement_bindings(child, bindings);
1335
1524
  }
@@ -1361,6 +1550,10 @@ function merge_binding_maps(outer, inner) {
1361
1550
  function function_has_native_tsrx_return(node) {
1362
1551
  if (!node) return false;
1363
1552
 
1553
+ if (node.body?.type === 'JSXCodeBlock') {
1554
+ return true;
1555
+ }
1556
+
1364
1557
  if (node.type === 'ArrowFunctionExpression' && node.body?.type !== 'BlockStatement') {
1365
1558
  return node_contains_native_tsrx_template(node.body);
1366
1559
  }
@@ -1396,22 +1589,23 @@ function statement_contains_native_tsrx_return(statement) {
1396
1589
  return statements_contain_native_tsrx_return(statement.body || []);
1397
1590
  }
1398
1591
 
1399
- if (statement.type === 'IfStatement') {
1592
+ if (is_if_control_node(statement)) {
1400
1593
  return (
1401
1594
  statement_contains_native_tsrx_return(statement.consequent) ||
1402
1595
  statement_contains_native_tsrx_return(statement.alternate)
1403
1596
  );
1404
1597
  }
1405
1598
 
1406
- if (statement.type === 'SwitchStatement') {
1599
+ if (is_switch_control_node(statement)) {
1407
1600
  return (statement.cases || []).some((/** @type {any} */ c) =>
1408
1601
  statements_contain_native_tsrx_return(c.consequent || []),
1409
1602
  );
1410
1603
  }
1411
1604
 
1412
- if (statement.type === 'TryStatement') {
1605
+ if (is_try_control_node(statement)) {
1413
1606
  return (
1414
1607
  statement_contains_native_tsrx_return(statement.block) ||
1608
+ statement_contains_native_tsrx_return(statement.pending) ||
1415
1609
  statement_contains_native_tsrx_return(statement.handler?.body) ||
1416
1610
  statement_contains_native_tsrx_return(statement.finalizer)
1417
1611
  );
@@ -1438,7 +1632,7 @@ function statement_contains_native_tsrx_return(statement) {
1438
1632
  */
1439
1633
  function node_contains_native_tsrx_template(node) {
1440
1634
  if (!node || typeof node !== 'object') return false;
1441
- if (node.type === 'Element' || node.type === 'TsrxFragment') return true;
1635
+ if (is_native_tsrx_node(node)) return true;
1442
1636
 
1443
1637
  if (is_function_or_class_boundary(node)) {
1444
1638
  return false;
@@ -1629,11 +1823,11 @@ function collect_style_elements(node, styles) {
1629
1823
  return;
1630
1824
  }
1631
1825
 
1632
- if (is_function_or_class_boundary(node) || node.type === 'TsrxFragment') {
1826
+ if (is_function_or_class_boundary(node)) {
1633
1827
  return;
1634
1828
  }
1635
1829
 
1636
- if (node.type === 'Element') {
1830
+ if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
1637
1831
  collect_style_elements(node.children || [], styles);
1638
1832
  return;
1639
1833
  }
@@ -1643,20 +1837,20 @@ function collect_style_elements(node, styles) {
1643
1837
  return;
1644
1838
  }
1645
1839
 
1646
- if (node.type === 'IfStatement') {
1840
+ if (is_if_control_node(node)) {
1647
1841
  collect_style_elements(node.consequent, styles);
1648
1842
  collect_style_elements(node.alternate, styles);
1649
1843
  return;
1650
1844
  }
1651
1845
 
1652
- if (node.type === 'SwitchStatement') {
1846
+ if (is_switch_control_node(node)) {
1653
1847
  for (const switch_case of node.cases || []) {
1654
1848
  collect_style_elements(switch_case.consequent || [], styles);
1655
1849
  }
1656
1850
  return;
1657
1851
  }
1658
1852
 
1659
- if (node.type === 'TryStatement') {
1853
+ if (is_try_control_node(node)) {
1660
1854
  collect_style_elements(node.block, styles);
1661
1855
  collect_style_elements(node.handler?.body, styles);
1662
1856
  collect_style_elements(node.finalizer, styles);
@@ -1708,7 +1902,7 @@ function strip_style_elements(node) {
1708
1902
  return node;
1709
1903
  }
1710
1904
 
1711
- if (node.type === 'Element') {
1905
+ if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
1712
1906
  node.children = strip_style_elements(node.children || []);
1713
1907
  return node;
1714
1908
  }
@@ -1718,20 +1912,20 @@ function strip_style_elements(node) {
1718
1912
  return node;
1719
1913
  }
1720
1914
 
1721
- if (node.type === 'IfStatement') {
1915
+ if (is_if_control_node(node)) {
1722
1916
  node.consequent = strip_style_elements(node.consequent);
1723
1917
  if (node.alternate) node.alternate = strip_style_elements(node.alternate);
1724
1918
  return node;
1725
1919
  }
1726
1920
 
1727
- if (node.type === 'SwitchStatement') {
1921
+ if (is_switch_control_node(node)) {
1728
1922
  for (const switch_case of node.cases || []) {
1729
1923
  switch_case.consequent = strip_style_elements(switch_case.consequent || []);
1730
1924
  }
1731
1925
  return node;
1732
1926
  }
1733
1927
 
1734
- if (node.type === 'TryStatement') {
1928
+ if (is_try_control_node(node)) {
1735
1929
  node.block = strip_style_elements(node.block);
1736
1930
  if (node.handler?.body) node.handler.body = strip_style_elements(node.handler.body);
1737
1931
  if (node.finalizer) node.finalizer = strip_style_elements(node.finalizer);
@@ -1747,9 +1941,7 @@ function strip_style_elements(node) {
1747
1941
  function is_style_expression_position(path) {
1748
1942
  const parent = path.at(-1);
1749
1943
  return !(
1750
- parent?.type === 'Element' ||
1751
- parent?.type === 'TsrxFragment' ||
1752
- parent?.type === 'TsxCompat' ||
1944
+ is_native_tsrx_node(parent) ||
1753
1945
  parent?.type === 'BlockStatement' ||
1754
1946
  parent?.type === 'Program' ||
1755
1947
  parent?.type === 'SwitchCase'
@@ -1804,9 +1996,11 @@ function create_native_tsrx_statement_list_block(block, transform_context) {
1804
1996
  function create_native_tsrx_render_statements(fragment, transform_context) {
1805
1997
  return with_tsrx_fragment_styles(fragment, transform_context, (style_context) => {
1806
1998
  const target = style_context?.fragment ?? fragment;
1999
+ const render_nodes =
2000
+ target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
1807
2001
  return [
1808
2002
  ...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
1809
- ...build_render_statements(get_tsrx_render_children(target), true, transform_context),
2003
+ ...build_render_statements(render_nodes, true, transform_context),
1810
2004
  ];
1811
2005
  });
1812
2006
  }
@@ -1836,7 +2030,7 @@ function expand_native_tsrx_return_statement_list(statements, transform_context)
1836
2030
  function expand_native_tsrx_return_statement(statement, transform_context) {
1837
2031
  if (!statement || typeof statement !== 'object') return [statement];
1838
2032
 
1839
- if (statement.type === 'ReturnStatement' && statement.argument?.type === 'TsrxFragment') {
2033
+ if (statement.type === 'ReturnStatement' && is_native_tsrx_node(statement.argument)) {
1840
2034
  return create_native_tsrx_render_statements(statement.argument, transform_context);
1841
2035
  }
1842
2036
 
@@ -1849,7 +2043,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1849
2043
  return body === statement.body ? [statement] : [b.block(body, statement)];
1850
2044
  }
1851
2045
 
1852
- if (statement.type === 'IfStatement') {
2046
+ if (is_if_control_node(statement)) {
1853
2047
  const consequent = expand_embedded_native_return_statement(
1854
2048
  statement.consequent,
1855
2049
  transform_context,
@@ -1863,7 +2057,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1863
2057
  return [set_loc(b.if(statement.test, consequent, alternate), statement)];
1864
2058
  }
1865
2059
 
1866
- if (statement.type === 'SwitchStatement') {
2060
+ if (is_switch_control_node(statement)) {
1867
2061
  let changed = false;
1868
2062
  const cases = (statement.cases || []).map((/** @type {any} */ switch_case) => {
1869
2063
  const consequent = expand_native_tsrx_return_statement_list(
@@ -1879,8 +2073,11 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1879
2073
  return changed ? [set_loc(b.switch(statement.discriminant, cases), statement)] : [statement];
1880
2074
  }
1881
2075
 
1882
- if (statement.type === 'TryStatement') {
2076
+ if (is_try_control_node(statement)) {
1883
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;
1884
2081
  const handler_body = statement.handler?.body
1885
2082
  ? expand_embedded_native_return_statement(statement.handler.body, transform_context)
1886
2083
  : statement.handler?.body;
@@ -1889,6 +2086,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1889
2086
  : statement.finalizer;
1890
2087
  if (
1891
2088
  block === statement.block &&
2089
+ pending === statement.pending &&
1892
2090
  handler_body === statement.handler?.body &&
1893
2091
  finalizer === statement.finalizer
1894
2092
  ) {
@@ -1903,7 +2101,7 @@ function expand_native_tsrx_return_statement(statement, transform_context) {
1903
2101
  statement.handler,
1904
2102
  )
1905
2103
  : statement.handler;
1906
- return [set_loc(b.try(block, handler, finalizer, statement.pending ?? null), statement)];
2104
+ return [set_loc(b.try(block, handler, finalizer, pending ?? null), statement)];
1907
2105
  }
1908
2106
 
1909
2107
  return [statement];
@@ -2036,7 +2234,7 @@ function node_contains_hook_bearing_tsrx(node, transform_context) {
2036
2234
  return node.some((child) => node_contains_hook_bearing_tsrx(child, transform_context));
2037
2235
  }
2038
2236
 
2039
- if (node.type === 'TsrxFragment') {
2237
+ if (is_native_tsrx_node(node)) {
2040
2238
  return body_contains_top_level_hook_call(node.children || [], transform_context, true);
2041
2239
  }
2042
2240
 
@@ -2083,7 +2281,7 @@ function should_extract_hook_helpers(transform_context) {
2083
2281
  */
2084
2282
  function create_module_scoped_hook_component_id(helper_id, transform_context) {
2085
2283
  return create_generated_identifier(
2086
- `${transform_context.helper_state?.base_name || 'TsrxFragment'}__${helper_id.name}`,
2284
+ `${transform_context.helper_state?.base_name || 'TSRXTemplate'}__${helper_id.name}`,
2087
2285
  );
2088
2286
  }
2089
2287
 
@@ -2301,6 +2499,48 @@ function expand_component_helpers(program) {
2301
2499
  return program;
2302
2500
  }
2303
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
+
2304
2544
  /**
2305
2545
  * Generated helper/statics metadata can be carried on function declarations,
2306
2546
  * variable declarations, object literal members, or export-safe expressions,
@@ -2383,11 +2623,7 @@ function create_component_return_statement(
2383
2623
  * @returns {boolean}
2384
2624
  */
2385
2625
  function is_loop_skip_return_statement(node) {
2386
- return (
2387
- node?.type === 'ReturnStatement' &&
2388
- node.argument == null &&
2389
- node.metadata?.generated_loop_continue_return === true
2390
- );
2626
+ return node?.type === 'ReturnStatement' && node.metadata?.generated_loop_continue_return === true;
2391
2627
  }
2392
2628
 
2393
2629
  /**
@@ -2403,7 +2639,7 @@ function is_loop_skip_if_statement(node) {
2403
2639
  * @returns {any[] | null}
2404
2640
  */
2405
2641
  function get_loop_skip_if_consequent_body(node) {
2406
- if (node?.type !== 'IfStatement' || node.alternate) {
2642
+ if (!is_if_control_node(node) || node.alternate) {
2407
2643
  return null;
2408
2644
  }
2409
2645
 
@@ -2424,7 +2660,15 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
2424
2660
  const branch_statements = build_render_statements(consequent_body, true, transform_context);
2425
2661
  prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
2426
2662
 
2427
- 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;
2428
2672
  }
2429
2673
 
2430
2674
  /**
@@ -2803,26 +3047,37 @@ function create_helper_props_type_literal_with_typeof_flags(bindings, aliases, u
2803
3047
  /**
2804
3048
  * @param {any} node
2805
3049
  * @param {TransformContext} transform_context
3050
+ * @param {boolean} [in_jsx_child]
2806
3051
  * @returns {any}
2807
3052
  */
2808
- function to_jsx_element(node, transform_context, raw_children = node.children || []) {
2809
- if (node.type === 'JSXElement') return node;
2810
- 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) {
2811
3066
  report_jsx_fragment_in_tsrx_error(node, transform_context);
2812
3067
  return set_loc(b.jsx_fragment(), node);
2813
3068
  }
2814
- if (is_dynamic_element_id(node.id)) {
2815
- 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);
2816
3071
  }
2817
3072
 
2818
- const name = identifier_to_jsx_name(node.id);
3073
+ const name = clone_jsx_name(source_name);
2819
3074
  const attributes = transform_element_attributes_dispatch(
2820
- node.attributes || [],
3075
+ source_opening.attributes || [],
2821
3076
  transform_context,
2822
3077
  node,
2823
3078
  );
2824
3079
  const walked_children = node.children || [];
2825
- let selfClosing = !!node.selfClosing;
3080
+ let selfClosing = !!source_opening.selfClosing;
2826
3081
  let children;
2827
3082
  const child_transform = transform_context.platform.hooks?.transformElementChildren?.(
2828
3083
  node,
@@ -2848,7 +3103,7 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
2848
3103
  name,
2849
3104
  attributes,
2850
3105
  selfClosing,
2851
- node.openingElement?.typeArguments,
3106
+ source_opening.typeArguments,
2852
3107
  );
2853
3108
  const openingElement = has_unmappable_attribute
2854
3109
  ? opening_element_node
@@ -3122,7 +3377,13 @@ function collect_block_binding_names_from_statement(statement, names) {
3122
3377
  return;
3123
3378
  }
3124
3379
 
3125
- 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
+ ) {
3126
3387
  if (statement.left?.type === 'VariableDeclaration' && statement.left.kind === 'var') {
3127
3388
  for (const declaration of statement.left.declarations || []) {
3128
3389
  collect_pattern_names(declaration.id, names);
@@ -3283,7 +3544,7 @@ function validate_hook_outer_assignments_in_node(
3283
3544
  }
3284
3545
  }
3285
3546
 
3286
- if (node.type === 'ForOfStatement') {
3547
+ if (is_for_of_control_node(node)) {
3287
3548
  if (
3288
3549
  node.left &&
3289
3550
  node.left.type !== 'VariableDeclaration' &&
@@ -3716,9 +3977,7 @@ function get_hook_callee_name(callee) {
3716
3977
  * @param {any} source_node
3717
3978
  * @param {TransformContext} transform_context
3718
3979
  * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
3719
- * Used by the switch lift's chained-call build, which allocates ids in
3720
- * source order in a forward pass and then constructs helpers in reverse so
3721
- * 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.
3722
3981
  * @param {{ transientBindings?: Set<string> }} [options]
3723
3982
  * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
3724
3983
  */
@@ -3997,6 +4256,118 @@ function get_body_source_node(body_nodes) {
3997
4256
  return first;
3998
4257
  }
3999
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
+
4000
4371
  /**
4001
4372
  * @param {any} node
4002
4373
  * @param {TransformContext} transform_context
@@ -4005,33 +4376,56 @@ function get_body_source_node(body_nodes) {
4005
4376
  function to_jsx_child(node, transform_context) {
4006
4377
  if (!node) return node;
4007
4378
  switch (node.type) {
4008
- case 'TsrxFragment':
4009
- return tsrx_node_to_jsx_expression(node, transform_context, true);
4010
- case 'TsxCompat':
4011
- return tsx_compat_node_to_jsx_expression(node, transform_context, true);
4012
- case 'Element':
4013
- return to_jsx_element(node, transform_context);
4014
- case 'Text':
4015
- return to_jsx_expression_container(to_text_expression(node.expression, node), node);
4016
- case 'TSRXExpression':
4017
- 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':
4018
4393
  case 'IfStatement':
4394
+ if (node.metadata?.generated_loop_skip_if) {
4395
+ return node;
4396
+ }
4019
4397
  return (
4020
4398
  transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
4021
- )(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);
4022
4414
  case 'ForOfStatement':
4023
4415
  return (
4024
4416
  transform_context.platform.hooks?.controlFlow?.forOf ?? for_of_statement_to_jsx_child
4025
4417
  )(node, transform_context);
4418
+ case 'JSXSwitchExpression':
4026
4419
  case 'SwitchStatement':
4027
4420
  return (
4028
4421
  transform_context.platform.hooks?.controlFlow?.switchStatement ??
4029
4422
  switch_statement_to_jsx_child
4030
- )(node, transform_context);
4423
+ )(jsx_control_expression_to_statement(node), transform_context);
4424
+ case 'JSXTryExpression':
4031
4425
  case 'TryStatement':
4032
4426
  return (
4033
4427
  transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
4034
- )(node, transform_context);
4428
+ )(jsx_control_expression_to_statement(node), transform_context);
4035
4429
  default:
4036
4430
  return node;
4037
4431
  }
@@ -4040,7 +4434,7 @@ function to_jsx_child(node, transform_context) {
4040
4434
  /**
4041
4435
  * Lower a native TSRX fragment body to a JSX expression.
4042
4436
  * Children have already been parsed and transformed through the normal TSRX
4043
- * Element/Text/control-flow visitors.
4437
+ * JSX element/text/control-flow visitors.
4044
4438
  *
4045
4439
  * @param {any} node
4046
4440
  * @param {TransformContext} transform_context
@@ -4123,7 +4517,7 @@ function return_value_statement_to_expression(node, transform_context) {
4123
4517
  return node.argument;
4124
4518
  }
4125
4519
 
4126
- if (node?.type === 'IfStatement') {
4520
+ if (is_if_control_node(node)) {
4127
4521
  return return_value_if_statement_to_conditional_expression(node, transform_context);
4128
4522
  }
4129
4523
 
@@ -4217,7 +4611,7 @@ function return_value_block_to_expression(node, transform_context) {
4217
4611
  * @returns {any | null}
4218
4612
  */
4219
4613
  function return_value_if_statement_to_conditional_expression(node, transform_context) {
4220
- if (!node || node.type !== 'IfStatement') return null;
4614
+ if (!is_if_control_node(node)) return null;
4221
4615
 
4222
4616
  const consequent = return_value_block_to_expression(node.consequent, transform_context);
4223
4617
  if (!consequent) return null;
@@ -4257,14 +4651,14 @@ function if_statement_to_jsx_child(node, transform_context) {
4257
4651
  * @returns {any | null}
4258
4652
  */
4259
4653
  function render_if_statement_to_conditional_expression(node) {
4260
- if (!node || node.type !== 'IfStatement') return null;
4654
+ if (!is_if_control_node(node)) return null;
4261
4655
 
4262
4656
  const consequent = block_statement_to_return_expression(node.consequent);
4263
4657
  if (!consequent) return null;
4264
4658
 
4265
4659
  let alternate = create_null_literal();
4266
4660
  if (node.alternate) {
4267
- if (node.alternate.type === 'IfStatement') {
4661
+ if (is_if_control_node(node.alternate)) {
4268
4662
  alternate = render_if_statement_to_conditional_expression(node.alternate);
4269
4663
  if (!alternate) return null;
4270
4664
  } else {
@@ -4301,25 +4695,11 @@ function block_statement_to_return_expression(block) {
4301
4695
  /**
4302
4696
  * Find the first `key` attribute expression in the top-level elements of a body.
4303
4697
  * Used to propagate keys from loop body elements to wrapper components.
4304
- * Works on both pre-transform (Ripple Element) and post-transform (JSXElement) nodes.
4305
- *
4306
4698
  * @param {any[]} body_nodes
4307
4699
  * @returns {any | undefined}
4308
4700
  */
4309
4701
  function find_key_expression_in_body(body_nodes) {
4310
4702
  for (const node of body_nodes) {
4311
- // Pre-transform: Ripple Element node
4312
- if (node.type === 'Element') {
4313
- for (const attr of node.attributes || []) {
4314
- if (attr.type === 'Attribute') {
4315
- const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
4316
- if (attr_name === 'key') {
4317
- return attr.value?.expression ?? attr.value;
4318
- }
4319
- }
4320
- }
4321
- }
4322
- // Post-transform: JSXElement node
4323
4703
  if (node.type === 'JSXElement') {
4324
4704
  for (const attr of node.openingElement?.attributes || []) {
4325
4705
  if (
@@ -4344,7 +4724,7 @@ function find_key_expression_in_body(body_nodes) {
4344
4724
  * @returns {any}
4345
4725
  */
4346
4726
  function continue_to_bare_return(source_node) {
4347
- const node = set_loc(b.return(null), source_node);
4727
+ const node = set_loc(b.return(create_null_literal()), source_node);
4348
4728
  node.metadata = {
4349
4729
  ...(node.metadata || {}),
4350
4730
  generated_loop_continue_return: true,
@@ -4354,8 +4734,9 @@ function continue_to_bare_return(source_node) {
4354
4734
 
4355
4735
  /**
4356
4736
  * `continue` in a component `for...of` body means "skip this item". JSX targets
4357
- * lower `for...of` to callbacks, so a raw ContinueStatement would be invalid JS;
4358
- * 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.
4359
4740
  *
4360
4741
  * @param {any[] | any} node
4361
4742
  * @param {boolean} [is_root]
@@ -4363,7 +4744,9 @@ function continue_to_bare_return(source_node) {
4363
4744
  */
4364
4745
  export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4365
4746
  if (Array.isArray(node)) {
4366
- 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
+ );
4367
4750
  }
4368
4751
 
4369
4752
  if (!node || typeof node !== 'object') {
@@ -4388,6 +4771,145 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4388
4771
  return node;
4389
4772
  }
4390
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
+
4391
4913
  /**
4392
4914
  * @param {any} node
4393
4915
  * @returns {boolean}
@@ -4395,8 +4917,11 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4395
4917
  function is_loop_statement(node) {
4396
4918
  return (
4397
4919
  node?.type === 'ForOfStatement' ||
4920
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForOfStatement') ||
4398
4921
  node?.type === 'ForStatement' ||
4922
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForStatement') ||
4399
4923
  node?.type === 'ForInStatement' ||
4924
+ (node?.type === 'JSXForExpression' && node.statementType === 'ForInStatement') ||
4400
4925
  node?.type === 'WhileStatement' ||
4401
4926
  node?.type === 'DoWhileStatement'
4402
4927
  );
@@ -4420,10 +4945,9 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4420
4945
 
4421
4946
  const loop_params = get_for_of_iteration_params(node.left, node.index);
4422
4947
  const loop_body = /** @type {any[]} */ (
4423
- rewrite_loop_continues_to_bare_returns(
4424
- node.body.type === 'BlockStatement' ? node.body.body : [node.body],
4425
- )
4948
+ node.body.type === 'BlockStatement' ? node.body.body : [node.body]
4426
4949
  );
4950
+ validate_for_body_control_flow(loop_body, transform_context);
4427
4951
  const has_hooks =
4428
4952
  should_extract_hook_helpers(transform_context) &&
4429
4953
  body_contains_top_level_hook_call(loop_body, transform_context, true);
@@ -4478,17 +5002,49 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4478
5002
  transform_context.available_bindings = saved_bindings;
4479
5003
 
4480
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;
4481
5022
 
4482
5023
  if (transform_context.platform.imports.forOfIterableHelper) {
4483
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) {
4484
5034
  return to_jsx_expression_container(
4485
- 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
+ ),
4486
5044
  );
4487
5045
  }
4488
5046
 
4489
- return to_jsx_expression_container(
4490
- b.call(b.member(node.right, create_generated_identifier('map')), iter_callback),
4491
- );
5047
+ return to_jsx_expression_container(map_call);
4492
5048
  }
4493
5049
 
4494
5050
  /**
@@ -4498,25 +5054,6 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4498
5054
  */
4499
5055
  function apply_key_to_loop_body(body_nodes, key_expression) {
4500
5056
  for (const node of body_nodes) {
4501
- if (node.type === 'Element') {
4502
- const attributes = node.attributes || (node.attributes = []);
4503
- const has_key = attributes.some((/** @type {any} */ attr) => {
4504
- const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
4505
- return attr_name === 'key';
4506
- });
4507
-
4508
- if (!has_key) {
4509
- attributes.push({
4510
- type: 'Attribute',
4511
- name: b.id('key'),
4512
- value: clone_expression_node(key_expression),
4513
- shorthand: false,
4514
- metadata: { path: [] },
4515
- });
4516
- }
4517
- return;
4518
- }
4519
-
4520
5057
  if (node.type === 'JSXElement') {
4521
5058
  const attributes = node.openingElement?.attributes || [];
4522
5059
  const has_key = attributes.some(
@@ -4546,7 +5083,7 @@ function apply_key_to_loop_body(body_nodes, key_expression) {
4546
5083
  function should_apply_key_to_loop_body(body_nodes) {
4547
5084
  let keyable_children = 0;
4548
5085
  for (const node of body_nodes) {
4549
- if (node.type === 'Element' || node.type === 'JSXElement') {
5086
+ if (node.type === 'JSXElement') {
4550
5087
  keyable_children += 1;
4551
5088
  }
4552
5089
  }
@@ -4636,11 +5173,11 @@ function switch_statement_to_jsx_child(node, transform_context) {
4636
5173
  }
4637
5174
 
4638
5175
  /**
4639
- * Transform a `try { ... } pending { ... } catch (err, reset) { ... }` block
5176
+ * Transform an `@try { ... } @pending { ... } @catch (err, reset) { ... }` block
4640
5177
  * into React `<TsrxErrorBoundary>` and/or `<Suspense>` JSX elements.
4641
5178
  *
4642
- * - `pending` → `<Suspense fallback={...}>`
4643
- * - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
5179
+ * - `@pending` → `<Suspense fallback={...}>`
5180
+ * - `@catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
4644
5181
  * - both → ErrorBoundary wraps Suspense
4645
5182
  * - JavaScript `try/finally` is not part of component template control flow
4646
5183
  *
@@ -4684,30 +5221,6 @@ function try_statement_to_jsx_child(node, transform_context) {
4684
5221
  );
4685
5222
  }
4686
5223
 
4687
- // Validate that try body contains JSX if pending block is present
4688
- if (pending) {
4689
- const try_body = node.block.body || [];
4690
- if (!try_body.some(is_jsx_child)) {
4691
- error(
4692
- 'TSRX try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
4693
- transform_context.filename,
4694
- node.block,
4695
- transform_context.errors,
4696
- transform_context.comments,
4697
- );
4698
- }
4699
- const pending_body = pending.body || [];
4700
- if (pending_body.length > 0 && !pending_body.some(is_jsx_child)) {
4701
- error(
4702
- 'TSRX try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
4703
- transform_context.filename,
4704
- pending,
4705
- transform_context.errors,
4706
- transform_context.comments,
4707
- );
4708
- }
4709
- }
4710
-
4711
5224
  // Build the try body content as JSX children
4712
5225
  const try_body_nodes = node.block.body || [];
4713
5226
  const try_content = statement_body_to_jsx_child(try_body_nodes, transform_context);
@@ -4860,8 +5373,8 @@ function create_jsx_element(tag_name, attributes, children) {
4860
5373
 
4861
5374
  /**
4862
5375
  * Inject runtime-helper import declarations the transform decided it needed
4863
- * during the walk: `Suspense` for `try { ... } pending { ... }`,
4864
- * `TsrxErrorBoundary` for `try { ... } catch (...)`, and `mergeRefs` for
5376
+ * during the walk: `Suspense` for `@try { ... } @pending { ... }`,
5377
+ * `TsrxErrorBoundary` for `@try { ... } @catch (...)`, and `mergeRefs` for
4865
5378
  * elements with multiple `ref` attributes under the `'merge-refs'`
4866
5379
  * strategy. Import sources are platform-specific.
4867
5380
  *
@@ -4975,16 +5488,22 @@ function add_ref_import_specifier(imports, source, specifier) {
4975
5488
  function create_render_if_statement(node, transform_context) {
4976
5489
  const consequent_body =
4977
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
+ }
4978
5494
  const consequent_has_hooks =
4979
5495
  should_extract_hook_helpers(transform_context) &&
4980
5496
  body_contains_top_level_hook_call(consequent_body, transform_context, true);
4981
5497
 
4982
5498
  let alternate = null;
4983
5499
  if (node.alternate) {
4984
- if (node.alternate.type === 'IfStatement') {
5500
+ if (is_if_control_node(node.alternate)) {
4985
5501
  alternate = create_render_if_statement(node.alternate, transform_context);
4986
5502
  } else {
4987
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
+ }
4988
5507
  const alternate_has_hooks =
4989
5508
  should_extract_hook_helpers(transform_context) &&
4990
5509
  body_contains_top_level_hook_call(alternate_body, transform_context, true);
@@ -5021,9 +5540,8 @@ function create_render_if_statement(node, transform_context) {
5021
5540
  * case body needs to be hoisted into its own helper component or can stay
5022
5541
  * inline.
5023
5542
  *
5024
- * `own_body` is everything in the case's `consequent` up to (and including for
5025
- * `return <expr>`, excluding for `break` / bare `return;`) the first
5026
- * 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.
5027
5545
  *
5028
5546
  * @param {any[]} consequent
5029
5547
  * @returns {{ own_body: any[], has_terminator: boolean }}
@@ -5032,10 +5550,6 @@ function summarize_switch_case_body(consequent) {
5032
5550
  const own_body = [];
5033
5551
  let has_terminator = false;
5034
5552
  for (const child of consequent) {
5035
- if (child.type === 'BreakStatement') {
5036
- has_terminator = true;
5037
- break;
5038
- }
5039
5553
  if (child.type === 'ReturnStatement' && child.argument == null) {
5040
5554
  has_terminator = true;
5041
5555
  break;
@@ -5068,11 +5582,10 @@ export function clone_switch_helper_invocation(helper) {
5068
5582
 
5069
5583
  /**
5070
5584
  * Plan the switch lift: decide which case bodies to hoist into their own
5071
- * helper components, build them in reverse so each helper can chain into the
5072
- * next, and return everything callers need to construct a target-specific
5073
- * switch shape (a JS `switch` for React/Preact/Vue or `<Switch>/<Match>` for
5074
- * Solid). Centralizes the lift bookkeeping so both consumers see the same
5075
- * 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.
5076
5589
  *
5077
5590
  * Returned helpers — when non-null — are already constructed via
5078
5591
  * `create_hook_safe_helper`, which is the same path hook-bearing case bodies
@@ -5086,7 +5599,6 @@ export function clone_switch_helper_invocation(helper) {
5086
5599
  * @returns {{
5087
5600
  * case_info: Array<{ own_body: any[], has_terminator: boolean }>,
5088
5601
  * case_helpers: Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>,
5089
- * find_next_helper_after: (from_index: number) => { component_element: ESTreeJSX.JSXElement } | null,
5090
5602
  * setup_statements: any[],
5091
5603
  * }}
5092
5604
  */
@@ -5096,22 +5608,15 @@ export function plan_switch_lift(switch_node, transform_context) {
5096
5608
  return summarize_switch_case_body(consequent);
5097
5609
  });
5098
5610
 
5099
- // A case body needs to be lifted iff (a) it would render in more than one
5100
- // arm after fall-through expansion, or (b) it contains hooks (which always
5101
- // went through the lift pipeline before this change). Duplication happens
5102
- // exactly when the previous case has no terminator — that's the only way
5103
- // 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.
5104
5613
  const needs_helper = case_info.map(
5105
- (/** @type {{ own_body: any[], has_terminator: boolean }} */ info, /** @type {number} */ k) => {
5614
+ (/** @type {{ own_body: any[], has_terminator: boolean }} */ info) => {
5106
5615
  if (info.own_body.length === 0) return false;
5107
- if (
5616
+ return (
5108
5617
  should_extract_hook_helpers(transform_context) &&
5109
5618
  body_contains_top_level_hook_call(info.own_body, transform_context, true)
5110
- ) {
5111
- return true;
5112
- }
5113
- if (k === 0) return false;
5114
- return !case_info[k - 1].has_terminator;
5619
+ );
5115
5620
  },
5116
5621
  );
5117
5622
 
@@ -5128,37 +5633,12 @@ export function plan_switch_lift(switch_node, transform_context) {
5128
5633
  /** @type {Array<{ setup_statements: any[], component_element: ESTreeJSX.JSXElement } | null>} */
5129
5634
  const case_helpers = new Array(switch_node.cases.length).fill(null);
5130
5635
 
5131
- /**
5132
- * Find the next downstream helper this arm chains into when it has no
5133
- * terminator: scan forward past any empty cases until we hit either a
5134
- * helper-bearing case or a case whose body has a terminator (which stops
5135
- * the chain — JS would have `break`/`return`ed out at that point).
5136
- *
5137
- * @param {number} from_index
5138
- * @returns {{ component_element: ESTreeJSX.JSXElement } | null}
5139
- */
5140
- function find_next_helper_after(from_index) {
5141
- for (let j = from_index + 1; j < switch_node.cases.length; j++) {
5142
- if (case_helpers[j]) return case_helpers[j];
5143
- if (case_info[j].has_terminator) return null;
5144
- }
5145
- return null;
5146
- }
5147
-
5148
5636
  for (let i = switch_node.cases.length - 1; i >= 0; i--) {
5149
5637
  if (!needs_helper[i]) continue;
5150
- const { own_body, has_terminator } = case_info[i];
5151
-
5152
- let helper_body = own_body;
5153
- if (!has_terminator) {
5154
- const next_helper = find_next_helper_after(i);
5155
- if (next_helper) {
5156
- helper_body = [...own_body, clone_switch_helper_invocation(next_helper)];
5157
- }
5158
- }
5638
+ const { own_body } = case_info[i];
5159
5639
 
5160
5640
  case_helpers[i] = create_hook_safe_helper(
5161
- helper_body,
5641
+ own_body,
5162
5642
  undefined,
5163
5643
  switch_node.cases[i],
5164
5644
  transform_context,
@@ -5176,40 +5656,17 @@ export function plan_switch_lift(switch_node, transform_context) {
5176
5656
  return {
5177
5657
  case_info,
5178
5658
  case_helpers,
5179
- find_next_helper_after,
5180
5659
  setup_statements,
5181
5660
  };
5182
5661
  }
5183
5662
 
5184
5663
  /**
5185
- * Switch lift for fall-through deduplication. Reuses the same `create_hook_safe_helper`
5186
- * pipeline as hook-bearing case bodies: every case whose body would otherwise
5187
- * appear in 2+ arms (because the previous case had no `break` / `return`) is
5188
- * hoisted into its own helper component, and each upstream arm references the
5189
- * next helper at the end of its own body to materialize JS fall-through at
5190
- * render time. Cases whose bodies live in exactly one arm stay inline so the
5191
- * common (break-terminated) shape compiles to the same simple switch as before
5192
- * the lift was introduced.
5193
- *
5194
- * The chain pattern:
5195
- * helper_idle = () => <><Online/><Helper_active/></>
5196
- * helper_active = () => <><Away/><Helper_offline/></>
5197
- * helper_offline = () => <Offline/>
5198
- *
5199
- * case "idle": return <Helper_idle/>
5200
- * case "active": return <Helper_active/>
5201
- * case "offline": return <Helper_offline/>
5202
- *
5203
- * Each case body appears exactly once in the generated module — matching how
5204
- * we already handle hook-bearing case bodies — which keeps the bundle from
5205
- * growing quadratically in case count and means editor mappings are 1:1.
5206
- *
5207
5664
  * @param {any} switch_node
5208
5665
  * @param {TransformContext} transform_context
5209
5666
  * @returns {{ setup_statements: any[], switch_statement: any }}
5210
5667
  */
5211
5668
  function build_switch_with_lift(switch_node, transform_context) {
5212
- 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(
5213
5670
  switch_node,
5214
5671
  transform_context,
5215
5672
  );
@@ -5229,10 +5686,10 @@ function build_switch_with_lift(switch_node, transform_context) {
5229
5686
  const { own_body, has_terminator } = case_info[i];
5230
5687
 
5231
5688
  if (own_body.length === 0 && !has_terminator) {
5232
- // Alias-pattern empty case (`case 'a': case 'b': ...`) — keep
5233
- // the arm body empty so JS falls through to the next case at
5234
- // runtime, where the helper invocation actually lives.
5235
- 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
+ );
5236
5693
  }
5237
5694
 
5238
5695
  const case_body = [];
@@ -5259,27 +5716,13 @@ function build_switch_with_lift(switch_node, transform_context) {
5259
5716
  }
5260
5717
  }
5261
5718
 
5262
- if (!has_terminal && !has_terminator) {
5263
- const next_helper = find_next_helper_after(i);
5264
- if (next_helper) {
5265
- render_nodes.push(clone_switch_helper_invocation(next_helper));
5266
- }
5267
- }
5268
-
5269
5719
  if (!has_terminal) {
5270
5720
  if (render_nodes.length > 0) {
5271
5721
  case_body.push(create_component_return_statement(render_nodes, original_case));
5272
- } else if (has_terminator) {
5273
- // Empty body with explicit `break;` / bare `return;` — keep
5274
- // a `break` so JS doesn't fall through into the next case
5275
- // (which may now hold the lifted helper invocation).
5276
- case_body.push(b.break);
5277
5722
  } else if (case_body.length > 0) {
5278
- // Statements-only inline case without terminator. We've
5279
- // already inlined the downstream chain via the helper
5280
- // reference above, so emit a `break` to stop the runtime
5281
- // from re-running downstream statements via JS fall-through.
5282
- 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());
5283
5726
  }
5284
5727
  }
5285
5728
 
@@ -5356,12 +5799,12 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
5356
5799
  * @returns {boolean}
5357
5800
  */
5358
5801
  export function is_component_like_element(element) {
5359
- const id = element?.id;
5360
- if (!id) return false;
5361
- if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
5362
- if (id.type === 'JSXIdentifier') return /^[A-Z]/.test(id.name);
5363
- if (id.type === 'MemberExpression') return true;
5364
- 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;
5365
5808
  return false;
5366
5809
  }
5367
5810
 
@@ -5514,10 +5957,7 @@ function wrap_jsx_setup_declarations(expression, in_jsx_child) {
5514
5957
  /**
5515
5958
  * Reject elements with more than one TSX-style `ref={...}` attribute.
5516
5959
  * This validator runs over the raw, pre-lowering attribute list so each
5517
- * shape is still distinguishable by `type`. Ripple `Element` attributes have type `Attribute` with an
5518
- * `Identifier` name (the parser normalizes `JSXAttribute`/`JSXIdentifier`
5519
- * for native elements); inside `<tsx:react>` compat blocks they retain
5520
- * the original `JSXAttribute`/`JSXIdentifier` shape, so we accept both.
5960
+ * shape is still distinguishable by `type`.
5521
5961
  *
5522
5962
  * @param {any[]} raw_attrs
5523
5963
  * @param {TransformContext} [transform_context]
@@ -5528,14 +5968,10 @@ export function validate_at_most_one_ref_attribute(raw_attrs, transform_context)
5528
5968
  for (const attr of raw_attrs) {
5529
5969
  if (!attr) continue;
5530
5970
  const is_ref_attr =
5531
- (attr.type === 'Attribute' &&
5532
- attr.name &&
5533
- attr.name.type === 'Identifier' &&
5534
- attr.name.name === 'ref') ||
5535
- (attr.type === 'JSXAttribute' &&
5536
- attr.name &&
5537
- attr.name.type === 'JSXIdentifier' &&
5538
- attr.name.name === 'ref');
5971
+ attr.type === 'JSXAttribute' &&
5972
+ attr.name &&
5973
+ attr.name.type === 'JSXIdentifier' &&
5974
+ attr.name.name === 'ref';
5539
5975
  if (!is_ref_attr) continue;
5540
5976
  refs.push(attr.name);
5541
5977
  }
@@ -5747,8 +6183,6 @@ function infer_ref_namespace(tag_name) {
5747
6183
  * @returns {string | null}
5748
6184
  */
5749
6185
  function get_element_ref_tag_name(element) {
5750
- const id = element?.id;
5751
- if (id?.type === 'Identifier') return id.name;
5752
6186
  const name = element?.name;
5753
6187
  if (name?.type === 'JSXIdentifier') return name.name;
5754
6188
  if (element?.openingElement?.name?.type === 'JSXIdentifier') {
@@ -5784,15 +6218,6 @@ export function to_jsx_attribute(attr, transform_context) {
5784
6218
  if (attr.type === 'JSXSpreadAttribute') {
5785
6219
  return attr;
5786
6220
  }
5787
- if (attr.type === 'SpreadAttribute') {
5788
- return set_loc(
5789
- /** @type {any} */ ({
5790
- type: 'JSXSpreadAttribute',
5791
- argument: attr.argument,
5792
- }),
5793
- attr,
5794
- );
5795
- }
5796
6221
  // Keep this legacy hook for targets that need React-style DOM attrs. The
5797
6222
  // current first-party targets preserve authored `class`.
5798
6223
  let attr_name = attr.name;
@@ -5844,25 +6269,29 @@ function value_has_unmappable_jsx_loc(value) {
5844
6269
  /**
5845
6270
  * @param {any} node
5846
6271
  * @param {TransformContext} transform_context
5847
- * @returns {ESTreeJSX.JSXExpressionContainer}
6272
+ * @param {boolean} in_jsx_child
6273
+ * @returns {any}
5848
6274
  */
5849
- function dynamic_element_to_jsx_child(node, transform_context) {
5850
- const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
5851
- 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
+ );
5852
6282
  const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
5853
6283
 
5854
- return to_jsx_expression_container(
5855
- b.call(
5856
- b.arrow(
5857
- [],
5858
- b.block([
5859
- alias_declaration,
5860
- b.return(b.conditional(clone_identifier(dynamic_id), jsx_element, create_null_literal())),
5861
- ]),
5862
- ),
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
+ ]),
5863
6291
  ),
5864
- node,
5865
6292
  );
6293
+
6294
+ return in_jsx_child ? to_jsx_expression_container(expression, node) : set_loc(expression, node);
5866
6295
  }
5867
6296
 
5868
6297
  /**
@@ -5873,11 +6302,11 @@ function dynamic_element_to_jsx_child(node, transform_context) {
5873
6302
  */
5874
6303
  function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
5875
6304
  const attributes = transform_element_attributes_dispatch(
5876
- node.attributes || [],
6305
+ node.openingElement?.attributes || [],
5877
6306
  transform_context,
5878
6307
  node,
5879
6308
  );
5880
- const selfClosing = !!node.selfClosing;
6309
+ const selfClosing = !!node.openingElement?.selfClosing;
5881
6310
  const children = create_element_children(node.children || [], transform_context);
5882
6311
  const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
5883
6312
 
@@ -5899,6 +6328,10 @@ function build_return_expression(render_nodes) {
5899
6328
  if (only.type === 'JSXExpressionContainer') {
5900
6329
  return only.expression;
5901
6330
  }
6331
+ if (only.type === 'JSXText') {
6332
+ const value = (only.value ?? '').trim();
6333
+ return b.literal(value, JSON.stringify(value), only);
6334
+ }
5902
6335
  return only;
5903
6336
  }
5904
6337
  const first = render_nodes[0];
@@ -5917,25 +6350,3 @@ function build_return_expression(render_nodes) {
5917
6350
  : undefined,
5918
6351
  );
5919
6352
  }
5920
-
5921
- /**
5922
- * @param {any} node
5923
- * @param {TransformContext} transform_context
5924
- * @param {boolean} [in_jsx_child]
5925
- * @returns {any}
5926
- */
5927
- function tsx_compat_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
5928
- const platform = transform_context.platform;
5929
- if (!platform.jsx.acceptedTsxKinds.includes(node.kind)) {
5930
- const accepted = platform.jsx.acceptedTsxKinds.map((k) => `<tsx:${k}>`).join(', ');
5931
- error(
5932
- `${platform.name} TSRX does not support <tsx:${node.kind}> blocks. Use one of: ${accepted}.`,
5933
- transform_context.filename,
5934
- node,
5935
- transform_context.errors,
5936
- transform_context.comments,
5937
- );
5938
- }
5939
-
5940
- return tsx_node_to_jsx_expression(node, in_jsx_child);
5941
- }