@tsrx/core 0.0.26 → 0.0.28

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.
@@ -20,12 +20,13 @@ import {
20
20
  flatten_switch_consequent,
21
21
  get_for_of_iteration_params,
22
22
  identifier_to_jsx_name,
23
+ is_bare_render_expression,
23
24
  is_dynamic_element_id,
24
25
  is_jsx_child,
25
26
  set_loc,
26
27
  to_text_expression,
27
28
  } from './ast-builders.js';
28
- import { render_stylesheets as renderStylesheets } from '../stylesheet.js';
29
+ import { render_css_result } from '../stylesheet.js';
29
30
  import {
30
31
  set_location as setLocation,
31
32
  jsx_attribute as build_jsx_attribute,
@@ -41,8 +42,10 @@ import {
41
42
  import { find_first_top_level_await_in_component_body } from '../await.js';
42
43
  import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
43
44
  import {
45
+ validate_class_component_declarations,
44
46
  validate_component_loop_break_statement,
45
47
  validate_component_loop_return_statement,
48
+ validate_component_params,
46
49
  validate_component_return_statement,
47
50
  validate_component_unsupported_loop_statement,
48
51
  } from '../../analyze/validation.js';
@@ -166,7 +169,11 @@ export function createJsxTransform(platform) {
166
169
  needs_error_boundary: false,
167
170
  needs_suspense: false,
168
171
  needs_merge_refs: false,
172
+ needs_ref_prop: false,
173
+ needs_normalize_spread_props: false,
169
174
  needs_fragment: false,
175
+ module_scoped_hook_components:
176
+ options?.moduleScopedHookComponents ?? !!platform.hooks?.moduleScopedHookComponents,
170
177
  helper_state: null,
171
178
  available_bindings: new Map(),
172
179
  lazy_next_id: 0,
@@ -175,12 +182,15 @@ export function createJsxTransform(platform) {
175
182
  collect,
176
183
  errors: collect ? options?.errors : undefined,
177
184
  comments: options?.comments,
185
+ typeOnly: !!options?.typeOnly,
178
186
  // Platforms can seed their own tracking state (e.g. solid's
179
187
  // needs_show / needs_for flags) via `hooks.initialState`.
180
188
  ...(platform.hooks?.initialState?.() ?? {}),
181
189
  };
182
190
 
183
- preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
191
+ if (!transform_context.typeOnly) {
192
+ preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
193
+ }
184
194
 
185
195
  walk(/** @type {any} */ (ast), transform_context, {
186
196
  ReturnStatement(node, { next, path }) {
@@ -270,9 +280,26 @@ export function createJsxTransform(platform) {
270
280
  return next();
271
281
  },
272
282
 
283
+ ClassBody(node, { next }) {
284
+ validate_class_component_declarations(
285
+ /** @type {any} */ (node),
286
+ filename,
287
+ transform_context.errors,
288
+ transform_context.comments,
289
+ );
290
+ return next();
291
+ },
292
+
273
293
  Component(node, { next, state }) {
274
294
  const as_any = /** @type {any} */ (node);
275
295
 
296
+ validate_component_params(
297
+ as_any,
298
+ filename,
299
+ transform_context.errors,
300
+ transform_context.comments,
301
+ );
302
+
276
303
  const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
277
304
 
278
305
  if (await_expression) {
@@ -357,13 +384,20 @@ export function createJsxTransform(platform) {
357
384
 
358
385
  Tsx(node, { next, path }) {
359
386
  const inner = /** @type {any} */ (next() ?? node);
360
- return /** @type {any} */ (tsx_node_to_jsx_expression(inner, in_jsx_child_context(path)));
387
+ const in_jsx_child = in_jsx_child_context(path);
388
+ return /** @type {any} */ (
389
+ wrap_jsx_setup_declarations(tsx_node_to_jsx_expression(inner, in_jsx_child), in_jsx_child)
390
+ );
361
391
  },
362
392
 
363
393
  TsxCompat(node, { next, path, state }) {
364
394
  const inner = /** @type {any} */ (next() ?? node);
395
+ const in_jsx_child = in_jsx_child_context(path);
365
396
  return /** @type {any} */ (
366
- tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child_context(path))
397
+ wrap_jsx_setup_declarations(
398
+ tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child),
399
+ in_jsx_child,
400
+ )
367
401
  );
368
402
  },
369
403
 
@@ -407,6 +441,27 @@ export function createJsxTransform(platform) {
407
441
  FunctionDeclaration: ensure_function_metadata,
408
442
  FunctionExpression: ensure_function_metadata,
409
443
  ArrowFunctionExpression: ensure_function_metadata,
444
+
445
+ RefExpression(node) {
446
+ return create_ref_prop_call(node, transform_context);
447
+ },
448
+
449
+ JSXOpeningElement(node, { next }) {
450
+ const visited = next() || node;
451
+ const is_component = is_component_like_jsx_name(visited.name);
452
+ const attrs = normalize_named_ref_attributes(
453
+ visited.attributes || [],
454
+ !is_component,
455
+ transform_context,
456
+ );
457
+ return {
458
+ ...visited,
459
+ attributes: merge_duplicate_refs(
460
+ normalize_host_ref_spreads(attrs, !is_component, transform_context),
461
+ transform_context,
462
+ ),
463
+ };
464
+ },
410
465
  });
411
466
 
412
467
  const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
@@ -420,8 +475,15 @@ export function createJsxTransform(platform) {
420
475
  // declarations, arrow functions, etc.). Component bodies have already been
421
476
  // transformed inside component_to_function_declaration; this catches plain
422
477
  // functions outside components and any lazy patterns in module scope.
478
+ // In type-only mode, the lazy patterns survive untouched: esrap ignores the
479
+ // non-standard `lazy` flag, so `&{ a, b }` prints as `{ a, b }`, `let &[a]
480
+ // = expr` prints as `let [a] = expr`, and the bare statement-level form
481
+ // `&[x] = expr;` (used when `x` is already declared) prints as `[x] =
482
+ // expr;` — a valid destructuring assignment to the existing binding.
423
483
  const final_program = /** @type {any} */ (
424
- apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
484
+ transform_context.typeOnly
485
+ ? expanded
486
+ : apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
425
487
  );
426
488
 
427
489
  const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
@@ -429,17 +491,11 @@ export function createJsxTransform(platform) {
429
491
  sourceMapContent: source,
430
492
  });
431
493
 
432
- const css =
433
- stylesheets.length > 0
434
- ? {
435
- code: renderStylesheets(
436
- /** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
437
- ),
438
- hash: stylesheets.map((s) => s.hash).join(' '),
439
- }
440
- : null;
494
+ const { css, cssHash } = render_css_result(
495
+ /** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
496
+ );
441
497
 
442
- return { ast: final_program, code: result.code, map: result.map, css };
498
+ return { ast: final_program, code: result.code, map: result.map, css, cssHash };
443
499
  }
444
500
 
445
501
  return transform;
@@ -503,7 +559,11 @@ export function component_to_function_declaration(component, transform_context,
503
559
  // Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
504
560
  // for later replacement. Body bindings (count, setCount, etc.) are still in the
505
561
  // original patterns, so collect_statement_bindings during build will find them.
506
- const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
562
+ // In type-only mode the lazy rewrite is skipped entirely so destructuring
563
+ // patterns survive into the virtual TSX and TypeScript can flow real types.
564
+ const lazy_bindings = transform_context.typeOnly
565
+ ? new Map()
566
+ : collect_lazy_bindings_from_component(params, body, transform_context);
507
567
 
508
568
  // Save and set context for this component scope
509
569
  const saved_helper_state = transform_context.helper_state;
@@ -876,6 +936,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
876
936
 
877
937
  if (is_jsx_child(child)) {
878
938
  const jsx = to_jsx_child(child, transform_context);
939
+ statements.push(...extract_jsx_setup_declarations(jsx));
879
940
  if (interleaved && is_capturable_jsx_child(jsx)) {
880
941
  const { declaration, reference } = captureJsxChild(jsx, capture_index++);
881
942
  statements.push(declaration);
@@ -883,6 +944,8 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
883
944
  } else {
884
945
  render_nodes.push(jsx);
885
946
  }
947
+ } else if (is_bare_render_expression(child)) {
948
+ render_nodes.push(to_jsx_expression_container(child, child));
886
949
  } else {
887
950
  statements.push(child);
888
951
  collect_statement_bindings(child, transform_context.available_bindings);
@@ -1170,6 +1233,25 @@ function create_helper_state(base_name) {
1170
1233
  };
1171
1234
  }
1172
1235
 
1236
+ /**
1237
+ * @param {TransformContext} transform_context
1238
+ * @returns {boolean}
1239
+ */
1240
+ function should_use_module_scoped_hook_components(transform_context) {
1241
+ return !!(transform_context.helper_state && transform_context.module_scoped_hook_components);
1242
+ }
1243
+
1244
+ /**
1245
+ * @param {AST.Identifier} helper_id
1246
+ * @param {TransformContext} transform_context
1247
+ * @returns {AST.Identifier}
1248
+ */
1249
+ function create_module_scoped_hook_component_id(helper_id, transform_context) {
1250
+ return create_generated_identifier(
1251
+ `${transform_context.helper_state?.base_name || 'Component'}__${helper_id.name}`,
1252
+ );
1253
+ }
1254
+
1173
1255
  /**
1174
1256
  * @param {any[]} params
1175
1257
  * @returns {Map<string, AST.Identifier>}
@@ -1525,9 +1607,7 @@ function create_component_return_statement(
1525
1607
  map_render_node_locations = true,
1526
1608
  ) {
1527
1609
  const cloned = render_nodes.map((node) =>
1528
- map_render_node_locations
1529
- ? clone_expression_node(node)
1530
- : clone_expression_node_without_locations(node),
1610
+ map_render_node_locations ? clone_expression_node(node) : clone_expression_node(node, false),
1531
1611
  );
1532
1612
 
1533
1613
  return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
@@ -1594,7 +1674,7 @@ function build_tail_helper(continuation_body, source_node, transform_context) {
1594
1674
  * @returns {any}
1595
1675
  */
1596
1676
  function clone_tail_invocation(tail_helper) {
1597
- return clone_expression_node_without_locations(tail_helper.component_element);
1677
+ return clone_expression_node(tail_helper.component_element, false);
1598
1678
  }
1599
1679
 
1600
1680
  /**
@@ -2149,25 +2229,33 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2149
2229
  const helper_id = create_generated_identifier(
2150
2230
  create_local_statement_component_name(transform_context),
2151
2231
  );
2152
-
2153
- const outer_aliases = outer_bindings.map((binding) =>
2154
- create_helper_type_alias_declaration(helper_id, binding),
2155
- );
2156
- const loop_aliases = loop_bindings.map((binding) =>
2157
- create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params),
2158
- );
2232
+ const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
2233
+ const component_id = use_module_scoped_component
2234
+ ? create_module_scoped_hook_component_id(helper_id, transform_context)
2235
+ : helper_id;
2236
+
2237
+ const outer_aliases = use_module_scoped_component
2238
+ ? []
2239
+ : outer_bindings.map((binding) => create_helper_type_alias_declaration(helper_id, binding));
2240
+ const loop_aliases = use_module_scoped_component
2241
+ ? []
2242
+ : loop_bindings.map((binding) =>
2243
+ create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params),
2244
+ );
2159
2245
 
2160
2246
  // Synthetic `isLast` prop on the loop helper when there's a tail. It's
2161
2247
  // passed from the .map callback as `i === source.length - 1` so every
2162
2248
  // loop-helper return can append the tail helper on the last iteration.
2163
2249
  const tail_isLast_alias = has_tail
2164
- ? {
2165
- id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2166
- declaration: b.ts_type_alias(
2167
- create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2168
- b.ts_keyword_type('boolean'),
2169
- ),
2170
- }
2250
+ ? use_module_scoped_component
2251
+ ? null
2252
+ : {
2253
+ id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2254
+ declaration: b.ts_type_alias(
2255
+ create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
2256
+ b.ts_keyword_type('boolean'),
2257
+ ),
2258
+ }
2171
2259
  : null;
2172
2260
 
2173
2261
  const ordered_bindings = [...outer_bindings, ...loop_bindings];
@@ -2181,7 +2269,7 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2181
2269
  const signature_use_typeof = has_tail ? [...ordered_use_typeof, false] : ordered_use_typeof;
2182
2270
 
2183
2271
  const props_type =
2184
- signature_bindings.length > 0
2272
+ signature_bindings.length > 0 && !use_module_scoped_component
2185
2273
  ? create_helper_props_type_literal_with_typeof_flags(
2186
2274
  signature_bindings,
2187
2275
  signature_aliases,
@@ -2189,7 +2277,13 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2189
2277
  )
2190
2278
  : null;
2191
2279
  const params =
2192
- props_type !== null ? [create_typed_helper_props_pattern(signature_bindings, props_type)] : [];
2280
+ signature_bindings.length > 0
2281
+ ? [
2282
+ props_type !== null
2283
+ ? create_typed_helper_props_pattern(signature_bindings, props_type)
2284
+ : create_helper_props_pattern(signature_bindings),
2285
+ ]
2286
+ : [];
2193
2287
 
2194
2288
  const fn_saved_bindings = transform_context.available_bindings;
2195
2289
  transform_context.available_bindings = new Map(fn_saved_bindings);
@@ -2207,12 +2301,17 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2207
2301
  transform_context.available_bindings = fn_saved_bindings;
2208
2302
 
2209
2303
  const helper_fn = /** @type {any} */ (
2210
- b.function(clone_identifier(helper_id), params, b.block(fn_body_statements))
2304
+ b.function(clone_identifier(component_id), params, b.block(fn_body_statements))
2211
2305
  );
2212
2306
  helper_fn.metadata = { path: [], is_component: true, is_method: false };
2213
2307
 
2214
2308
  let helper_decl;
2215
- if (transform_context.helper_state) {
2309
+ if (transform_context.helper_state && use_module_scoped_component) {
2310
+ transform_context.helper_state.helpers.push(
2311
+ create_helper_declaration(component_id, helper_fn, node, transform_context),
2312
+ );
2313
+ helper_decl = null;
2314
+ } else if (transform_context.helper_state) {
2216
2315
  const cache_id = create_generated_identifier(
2217
2316
  `${transform_context.helper_state.base_name}__${helper_id.name}`,
2218
2317
  );
@@ -2229,7 +2328,7 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2229
2328
  transform_context.available_bindings = saved_bindings;
2230
2329
 
2231
2330
  const callback_invocation_element = create_helper_component_element(
2232
- helper_id,
2331
+ component_id,
2233
2332
  ordered_bindings,
2234
2333
  node,
2235
2334
  { mapWrapper: false, mapBindingNames: false, mapBindingValues: false },
@@ -2310,7 +2409,9 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2310
2409
  if (has_tail && tail_isLast_alias) {
2311
2410
  hoist_statements.push(tail_isLast_alias.declaration);
2312
2411
  }
2313
- hoist_statements.push(helper_decl);
2412
+ if (helper_decl) {
2413
+ hoist_statements.push(helper_decl);
2414
+ }
2314
2415
 
2315
2416
  return {
2316
2417
  hoist_statements,
@@ -2481,7 +2582,7 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
2481
2582
  * @returns {any}
2482
2583
  */
2483
2584
  function combine_render_return_argument(render_nodes, return_argument) {
2484
- const combined = render_nodes.map((node) => clone_expression_node_without_locations(node));
2585
+ const combined = render_nodes.map((node) => clone_expression_node(node, false));
2485
2586
 
2486
2587
  if (return_argument != null && !is_null_literal(return_argument)) {
2487
2588
  combined.push(return_argument_to_render_node(return_argument));
@@ -2514,30 +2615,6 @@ function is_null_literal(node) {
2514
2615
  return node?.type === 'Literal' && node.value == null;
2515
2616
  }
2516
2617
 
2517
- /**
2518
- * @param {any} node
2519
- * @returns {any}
2520
- */
2521
- function clone_expression_node_without_locations(node) {
2522
- if (!node || typeof node !== 'object') return node;
2523
- if (Array.isArray(node)) return node.map(clone_expression_node_without_locations);
2524
-
2525
- const clone = { ...node };
2526
- delete clone.loc;
2527
- delete clone.start;
2528
- delete clone.end;
2529
-
2530
- for (const key of Object.keys(clone)) {
2531
- if (key === 'metadata') {
2532
- clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
2533
- continue;
2534
- }
2535
- clone[key] = clone_expression_node_without_locations(clone[key]);
2536
- }
2537
-
2538
- return clone;
2539
- }
2540
-
2541
2618
  const TEMPLATE_FRAGMENT_ERROR =
2542
2619
  '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>.';
2543
2620
 
@@ -2770,8 +2847,8 @@ function create_local_statement_component_name(transform_context) {
2770
2847
  /**
2771
2848
  * Wraps a list of body nodes into a component and returns
2772
2849
  * statements that return `<ComponentName prop1={prop1} ... />`.
2773
- * The component is hoisted to module level via helper_state to avoid
2774
- * recreating the component identity on every render.
2850
+ * Targets can either emit the helper component at module scope or cache the
2851
+ * component identity in module state while initializing it from the parent.
2775
2852
  * Used when a control flow branch contains hook calls that must be moved
2776
2853
  * into their own component boundary to satisfy the Rules of Hooks.
2777
2854
  *
@@ -2844,24 +2921,36 @@ function create_hook_safe_helper(
2844
2921
  const helper_id =
2845
2922
  preallocated_helper_id ??
2846
2923
  create_generated_identifier(create_local_statement_component_name(transform_context));
2924
+ const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
2925
+ const component_id = use_module_scoped_component
2926
+ ? create_module_scoped_hook_component_id(helper_id, transform_context)
2927
+ : helper_id;
2847
2928
  const helper_bindings = get_referenced_helper_bindings(
2848
2929
  body_nodes,
2849
2930
  transform_context.available_bindings,
2850
2931
  );
2851
- const aliases = helper_bindings.map((binding) =>
2852
- create_helper_type_alias_declaration(helper_id, binding),
2853
- );
2932
+ const aliases = use_module_scoped_component
2933
+ ? []
2934
+ : helper_bindings.map((binding) => create_helper_type_alias_declaration(helper_id, binding));
2854
2935
  const props_type =
2855
- helper_bindings.length > 0 ? create_helper_props_type_literal(helper_bindings, aliases) : null;
2936
+ helper_bindings.length > 0 && !use_module_scoped_component
2937
+ ? create_helper_props_type_literal(helper_bindings, aliases)
2938
+ : null;
2856
2939
  const params =
2857
- props_type !== null ? [create_typed_helper_props_pattern(helper_bindings, props_type)] : [];
2940
+ helper_bindings.length > 0
2941
+ ? [
2942
+ props_type !== null
2943
+ ? create_typed_helper_props_pattern(helper_bindings, props_type)
2944
+ : create_helper_props_pattern(helper_bindings),
2945
+ ]
2946
+ : [];
2858
2947
 
2859
2948
  const saved_bindings = transform_context.available_bindings;
2860
2949
  transform_context.available_bindings = new Map(saved_bindings);
2861
2950
 
2862
2951
  const helper_fn = /** @type {any} */ ({
2863
2952
  type: 'FunctionExpression',
2864
- id: clone_identifier(helper_id),
2953
+ id: clone_identifier(component_id),
2865
2954
  params,
2866
2955
  body: {
2867
2956
  type: 'BlockStatement',
@@ -2880,7 +2969,7 @@ function create_hook_safe_helper(
2880
2969
  transform_context.available_bindings = saved_bindings;
2881
2970
 
2882
2971
  const component_element = create_helper_component_element(
2883
- helper_id,
2972
+ component_id,
2884
2973
  helper_bindings,
2885
2974
  source_node,
2886
2975
  {
@@ -2911,6 +3000,16 @@ function create_hook_safe_helper(
2911
3000
  };
2912
3001
  }
2913
3002
 
3003
+ if (use_module_scoped_component) {
3004
+ transform_context.helper_state.helpers.push(
3005
+ create_helper_declaration(component_id, helper_fn, source_node, transform_context),
3006
+ );
3007
+ return {
3008
+ setup_statements: [],
3009
+ component_element,
3010
+ };
3011
+ }
3012
+
2914
3013
  const cache_id = create_generated_identifier(
2915
3014
  `${transform_context.helper_state.base_name}__${helper_id.name}`,
2916
3015
  );
@@ -4107,30 +4206,79 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
4107
4206
  });
4108
4207
  }
4109
4208
 
4110
- if (transform_context.needs_merge_refs && platform.imports.mergeRefs) {
4111
- const merge_refs_source = platform.imports.mergeRefs;
4209
+ const merge_refs_source =
4210
+ transform_context.needs_merge_refs && platform.imports.mergeRefs
4211
+ ? platform.imports.mergeRefs
4212
+ : null;
4213
+ const ref_prop_source =
4214
+ transform_context.needs_ref_prop && platform.imports.refProp ? platform.imports.refProp : null;
4215
+ const normalize_spread_props_source =
4216
+ transform_context.needs_normalize_spread_props && platform.imports.refProp
4217
+ ? platform.imports.refProp
4218
+ : null;
4219
+
4220
+ /** @type {Map<string, any[]>} */
4221
+ const ref_imports = new Map();
4222
+
4223
+ if (merge_refs_source !== null) {
4224
+ add_ref_import_specifier(ref_imports, merge_refs_source, {
4225
+ type: 'ImportSpecifier',
4226
+ imported: {
4227
+ type: 'Identifier',
4228
+ name: 'mergeRefs',
4229
+ metadata: { path: [] },
4230
+ },
4231
+ local: {
4232
+ type: 'Identifier',
4233
+ name: MERGE_REFS_INTERNAL_NAME,
4234
+ metadata: { path: [] },
4235
+ },
4236
+ metadata: { path: [] },
4237
+ });
4238
+ }
4239
+
4240
+ if (ref_prop_source !== null) {
4241
+ add_ref_import_specifier(ref_imports, ref_prop_source, {
4242
+ type: 'ImportSpecifier',
4243
+ imported: {
4244
+ type: 'Identifier',
4245
+ name: 'create_ref_prop',
4246
+ metadata: { path: [] },
4247
+ },
4248
+ local: {
4249
+ type: 'Identifier',
4250
+ name: CREATE_REF_PROP_INTERNAL_NAME,
4251
+ metadata: { path: [] },
4252
+ },
4253
+ metadata: { path: [] },
4254
+ });
4255
+ }
4256
+
4257
+ if (normalize_spread_props_source !== null) {
4258
+ add_ref_import_specifier(ref_imports, normalize_spread_props_source, {
4259
+ type: 'ImportSpecifier',
4260
+ imported: {
4261
+ type: 'Identifier',
4262
+ name: 'normalize_spread_props',
4263
+ metadata: { path: [] },
4264
+ },
4265
+ local: {
4266
+ type: 'Identifier',
4267
+ name: NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
4268
+ metadata: { path: [] },
4269
+ },
4270
+ metadata: { path: [] },
4271
+ });
4272
+ }
4273
+
4274
+ for (const [source, ref_specifiers] of ref_imports) {
4112
4275
  imports.push({
4113
4276
  type: 'ImportDeclaration',
4114
- specifiers: [
4115
- {
4116
- type: 'ImportSpecifier',
4117
- imported: {
4118
- type: 'Identifier',
4119
- name: 'mergeRefs',
4120
- metadata: { path: [] },
4121
- },
4122
- local: {
4123
- type: 'Identifier',
4124
- name: MERGE_REFS_LOCAL_NAME,
4125
- metadata: { path: [] },
4126
- },
4127
- metadata: { path: [] },
4128
- },
4129
- ],
4277
+ specifiers: ref_specifiers,
4130
4278
  source: {
4131
4279
  type: 'Literal',
4132
- value: merge_refs_source,
4133
- raw: `'${merge_refs_source}'`,
4280
+ value: source,
4281
+ raw: `'${source}'`,
4134
4282
  },
4135
4283
  metadata: { path: [] },
4136
4284
  });
@@ -4141,6 +4289,20 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
4141
4289
  }
4142
4290
  }
4143
4291
 
4292
+ /**
4293
+ * @param {Map<string, any[]>} imports
4294
+ * @param {string} source
4295
+ * @param {any} specifier
4296
+ */
4297
+ function add_ref_import_specifier(imports, source, specifier) {
4298
+ const specifiers = imports.get(source);
4299
+ if (specifiers) {
4300
+ specifiers.push(specifier);
4301
+ } else {
4302
+ imports.set(source, [specifier]);
4303
+ }
4304
+ }
4305
+
4144
4306
  /**
4145
4307
  * @param {any} node
4146
4308
  * @param {TransformContext} transform_context
@@ -4262,6 +4424,8 @@ function create_render_switch_case(switch_case, transform_context) {
4262
4424
 
4263
4425
  if (is_jsx_child(child)) {
4264
4426
  render_nodes.push(to_jsx_child(child, transform_context));
4427
+ } else if (is_bare_render_expression(child)) {
4428
+ render_nodes.push(to_jsx_expression_container(child, child));
4265
4429
  } else {
4266
4430
  case_body.push(child);
4267
4431
  }
@@ -4325,6 +4489,8 @@ function to_jsx_expression_container(expression, source_node = expression) {
4325
4489
  */
4326
4490
  function transform_element_attributes_dispatch(attrs, transform_context, element) {
4327
4491
  validate_at_most_one_ref_attribute(attrs, transform_context);
4492
+ const is_component = is_component_like_element(element);
4493
+ attrs = normalize_named_ref_attributes(attrs, !is_component, transform_context);
4328
4494
  const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
4329
4495
  if (preprocess) {
4330
4496
  attrs = preprocess(attrs, transform_context, element);
@@ -4333,7 +4499,238 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
4333
4499
  const result = hook
4334
4500
  ? hook(attrs, transform_context, element)
4335
4501
  : attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
4336
- return merge_duplicate_refs(result, transform_context);
4502
+ return merge_duplicate_refs(
4503
+ normalize_host_ref_spreads(result, !is_component, transform_context),
4504
+ transform_context,
4505
+ );
4506
+ }
4507
+
4508
+ /**
4509
+ * @param {any} element
4510
+ * @returns {boolean}
4511
+ */
4512
+ function is_component_like_element(element) {
4513
+ const id = element?.id;
4514
+ if (!id) return false;
4515
+ if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
4516
+ if (id.type === 'JSXIdentifier') return /^[A-Z]/.test(id.name);
4517
+ if (id.type === 'MemberExpression') return true;
4518
+ if (id.type === 'JSXMemberExpression') return true;
4519
+ return false;
4520
+ }
4521
+
4522
+ /**
4523
+ * @param {any} name
4524
+ * @returns {boolean}
4525
+ */
4526
+ function is_component_like_jsx_name(name) {
4527
+ if (!name) return false;
4528
+ if (name.type === 'JSXIdentifier') return /^[A-Z]/.test(name.name);
4529
+ if (name.type === 'JSXMemberExpression') return true;
4530
+ return false;
4531
+ }
4532
+
4533
+ /**
4534
+ * @param {any[]} attrs
4535
+ * @param {boolean} is_host
4536
+ * @param {TransformContext} transform_context
4537
+ * @returns {any[]}
4538
+ */
4539
+ function normalize_named_ref_attributes(attrs, is_host, transform_context) {
4540
+ if (!is_host) return attrs;
4541
+
4542
+ return attrs.map((attr) => {
4543
+ if (!is_named_ref_attribute(attr)) {
4544
+ return attr;
4545
+ }
4546
+
4547
+ if (transform_context.typeOnly) {
4548
+ return mark_type_only_named_ref_attribute(attr);
4549
+ }
4550
+
4551
+ return {
4552
+ ...attr,
4553
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4554
+ name:
4555
+ attr.name?.type === 'JSXIdentifier'
4556
+ ? { ...attr.name, name: 'ref' }
4557
+ : { type: 'Identifier', name: 'ref', metadata: { path: [] } },
4558
+ };
4559
+ });
4560
+ }
4561
+
4562
+ /**
4563
+ * @param {any} attr
4564
+ * @returns {any}
4565
+ */
4566
+ function mark_type_only_named_ref_attribute(attr) {
4567
+ return {
4568
+ ...attr,
4569
+ name: attr.name
4570
+ ? {
4571
+ ...attr.name,
4572
+ metadata: { ...(attr.name.metadata || {}), disable_verification: true },
4573
+ }
4574
+ : attr.name,
4575
+ };
4576
+ }
4577
+
4578
+ /**
4579
+ * @param {any[]} attrs
4580
+ * @param {boolean} is_host
4581
+ * @param {TransformContext} transform_context
4582
+ * @returns {any[]}
4583
+ */
4584
+ function normalize_host_ref_spreads(attrs, is_host, transform_context) {
4585
+ if (!is_host) return attrs;
4586
+
4587
+ const needs_explicit_spread_ref =
4588
+ transform_context.platform.jsx?.hostSpreadRefStrategy === 'explicit-ref-attr';
4589
+ const ref_exprs = attrs
4590
+ .filter((attr) => is_jsx_ref_attribute(attr))
4591
+ .map((attr) => attr.value.expression);
4592
+ const needs_synthetic_spread_ref = needs_explicit_spread_ref || ref_exprs.length > 0;
4593
+
4594
+ return attrs.flatMap((attr) => {
4595
+ if (!attr || attr.type !== 'JSXSpreadAttribute') {
4596
+ return [attr];
4597
+ }
4598
+
4599
+ transform_context.needs_normalize_spread_props = true;
4600
+ const normalized = b.call(NORMALIZE_SPREAD_PROPS_INTERNAL_NAME, attr.argument);
4601
+
4602
+ if (needs_synthetic_spread_ref) {
4603
+ const normalized_id = create_generated_identifier(
4604
+ create_spread_props_name(transform_context),
4605
+ );
4606
+ const spread = {
4607
+ ...attr,
4608
+ argument: clone_identifier(normalized_id),
4609
+ };
4610
+ const ref_attr = b.jsx_attribute(
4611
+ b.jsx_id('ref'),
4612
+ to_jsx_expression_container(b.member(clone_identifier(normalized_id), 'ref'), attr),
4613
+ false,
4614
+ attr,
4615
+ );
4616
+ ref_attr.metadata = { ...(ref_attr.metadata || {}) };
4617
+ /** @type {any} */ (ref_attr.metadata).from_ref_keyword = true;
4618
+ add_jsx_setup_declaration(spread, b.let(clone_identifier(normalized_id), normalized));
4619
+
4620
+ return [spread, ref_attr];
4621
+ }
4622
+
4623
+ return [
4624
+ {
4625
+ ...attr,
4626
+ argument: normalized,
4627
+ },
4628
+ ];
4629
+ });
4630
+ }
4631
+
4632
+ /**
4633
+ * @param {TransformContext} transform_context
4634
+ * @returns {string}
4635
+ */
4636
+ function create_spread_props_name(transform_context) {
4637
+ if (transform_context.helper_state) {
4638
+ return create_helper_name(transform_context.helper_state, 'spread_props');
4639
+ }
4640
+
4641
+ transform_context.local_statement_component_index += 1;
4642
+ return `_tsrx_spread_props_${transform_context.local_statement_component_index}`;
4643
+ }
4644
+
4645
+ /**
4646
+ * @param {any} node
4647
+ * @param {any} declaration
4648
+ */
4649
+ export function add_jsx_setup_declaration(node, declaration) {
4650
+ node.metadata ??= { path: [] };
4651
+ (node.metadata.generated_setup_declarations ??= []).push(declaration);
4652
+ }
4653
+
4654
+ /**
4655
+ * @param {any} node
4656
+ * @param {Set<any>} [seen]
4657
+ * @returns {any[]}
4658
+ */
4659
+ export function extract_jsx_setup_declarations(node, seen = new Set()) {
4660
+ if (node == null || typeof node !== 'object' || seen.has(node)) {
4661
+ return [];
4662
+ }
4663
+ seen.add(node);
4664
+
4665
+ const declarations = node.metadata?.generated_setup_declarations ?? [];
4666
+ if (node.metadata?.generated_setup_declarations) {
4667
+ delete node.metadata.generated_setup_declarations;
4668
+ }
4669
+
4670
+ for (const key of Object.keys(node)) {
4671
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4672
+ continue;
4673
+ }
4674
+ declarations.push(...extract_jsx_setup_declarations(node[key], seen));
4675
+ }
4676
+
4677
+ return declarations;
4678
+ }
4679
+
4680
+ /**
4681
+ * @param {any} expression
4682
+ * @param {boolean} in_jsx_child
4683
+ * @returns {any}
4684
+ */
4685
+ function wrap_jsx_setup_declarations(expression, in_jsx_child) {
4686
+ const declarations = extract_jsx_setup_declarations(expression);
4687
+ if (declarations.length === 0) {
4688
+ return expression;
4689
+ }
4690
+
4691
+ const return_expression =
4692
+ expression?.type === 'JSXExpressionContainer' ? expression.expression : expression;
4693
+ const call = b.call(
4694
+ b.arrow(
4695
+ [],
4696
+ b.block([...declarations, b.return(return_expression)], expression),
4697
+ false,
4698
+ expression,
4699
+ ),
4700
+ );
4701
+
4702
+ return in_jsx_child ? to_jsx_expression_container(call, expression) : call;
4703
+ }
4704
+
4705
+ /**
4706
+ * @param {any} attr
4707
+ * @returns {boolean}
4708
+ */
4709
+ function is_named_ref_attribute(attr) {
4710
+ return !!(
4711
+ attr &&
4712
+ (attr.type === 'Attribute' || attr.type === 'JSXAttribute') &&
4713
+ attr.name &&
4714
+ ((attr.name.type === 'Identifier' && attr.name.name !== 'ref') ||
4715
+ (attr.name.type === 'JSXIdentifier' && attr.name.name !== 'ref')) &&
4716
+ (attr.value?.type === 'RefExpression' ||
4717
+ is_ref_prop_expression(attr.value) ||
4718
+ (attr.value?.type === 'JSXExpressionContainer' &&
4719
+ is_ref_prop_expression(attr.value.expression)))
4720
+ );
4721
+ }
4722
+
4723
+ /**
4724
+ * @param {any} expression
4725
+ * @returns {boolean}
4726
+ */
4727
+ export function is_ref_prop_expression(expression) {
4728
+ return (
4729
+ expression?.type === 'RefExpression' ||
4730
+ (expression?.type === 'CallExpression' &&
4731
+ expression.callee?.type === 'Identifier' &&
4732
+ expression.callee.name === CREATE_REF_PROP_INTERNAL_NAME)
4733
+ );
4337
4734
  }
4338
4735
 
4339
4736
  /**
@@ -4453,7 +4850,7 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
4453
4850
  type: 'CallExpression',
4454
4851
  callee: {
4455
4852
  type: 'Identifier',
4456
- name: MERGE_REFS_LOCAL_NAME,
4853
+ name: MERGE_REFS_INTERNAL_NAME,
4457
4854
  metadata: { path: [] },
4458
4855
  },
4459
4856
  arguments: ref_exprs,
@@ -4514,7 +4911,9 @@ function is_jsx_ref_attribute(attr) {
4514
4911
  * double-underscore matches the convention for compiler-generated
4515
4912
  * identifiers and avoids shadowing user-declared `mergeRefs` symbols.
4516
4913
  */
4517
- const MERGE_REFS_LOCAL_NAME = '__mergeRefs';
4914
+ export const MERGE_REFS_INTERNAL_NAME = '__mergeRefs';
4915
+ export const CREATE_REF_PROP_INTERNAL_NAME = '__create_ref_prop';
4916
+ export const NORMALIZE_SPREAD_PROPS_INTERNAL_NAME = '__normalize_spread_props';
4518
4917
 
4519
4918
  /**
4520
4919
  * @param {any} attr
@@ -4523,7 +4922,31 @@ const MERGE_REFS_LOCAL_NAME = '__mergeRefs';
4523
4922
  */
4524
4923
  export function to_jsx_attribute(attr, transform_context) {
4525
4924
  if (!attr) return attr;
4526
- if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') {
4925
+ if (attr.type === 'JSXAttribute') {
4926
+ if (
4927
+ attr.value?.type === 'JSXExpressionContainer' &&
4928
+ attr.value.expression?.type === 'RefExpression'
4929
+ ) {
4930
+ return {
4931
+ ...attr,
4932
+ value: to_jsx_expression_container(
4933
+ create_ref_prop_call(attr.value.expression, transform_context),
4934
+ ),
4935
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4936
+ };
4937
+ }
4938
+ if (
4939
+ attr.value?.type === 'JSXExpressionContainer' &&
4940
+ is_ref_prop_expression(attr.value.expression)
4941
+ ) {
4942
+ return {
4943
+ ...attr,
4944
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4945
+ };
4946
+ }
4947
+ return attr;
4948
+ }
4949
+ if (attr.type === 'JSXSpreadAttribute') {
4527
4950
  return attr;
4528
4951
  }
4529
4952
  if (attr.type === 'SpreadAttribute') {
@@ -4574,15 +4997,28 @@ export function to_jsx_attribute(attr, transform_context) {
4574
4997
  attr_name && attr_name.type === 'Identifier' ? identifier_to_jsx_name(attr_name) : attr_name;
4575
4998
 
4576
4999
  let value = attr.value;
5000
+ const is_ref_expression_value =
5001
+ value?.type === 'RefExpression' ||
5002
+ is_ref_prop_expression(value) ||
5003
+ (value?.type === 'JSXExpressionContainer' && is_ref_prop_expression(value.expression));
4577
5004
  if (value) {
4578
5005
  if (value.type === 'Literal' && typeof value.value === 'string') {
4579
5006
  // Keep string literal as attribute string.
5007
+ } else if (value.type === 'RefExpression') {
5008
+ value = to_jsx_expression_container(create_ref_prop_call(value, transform_context));
4580
5009
  } else if (value.type !== 'JSXExpressionContainer') {
4581
5010
  value = to_jsx_expression_container(value);
5011
+ } else if (value.expression?.type === 'RefExpression') {
5012
+ value = to_jsx_expression_container(
5013
+ create_ref_prop_call(value.expression, transform_context),
5014
+ );
4582
5015
  }
4583
5016
  }
4584
5017
 
4585
5018
  const jsx_attribute = build_jsx_attribute(name, value || null, attr.shorthand === true);
5019
+ if (is_ref_expression_value) {
5020
+ /** @type {any} */ (jsx_attribute.metadata).from_ref_keyword = true;
5021
+ }
4586
5022
 
4587
5023
  if (value_has_unmappable_jsx_loc(value)) {
4588
5024
  /** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
@@ -4604,6 +5040,35 @@ function value_has_unmappable_jsx_loc(value) {
4604
5040
  );
4605
5041
  }
4606
5042
 
5043
+ /**
5044
+ * @param {any} node
5045
+ * @param {TransformContext} transform_context
5046
+ * @returns {any}
5047
+ */
5048
+ function create_ref_prop_call(node, transform_context) {
5049
+ transform_context.needs_ref_prop = true;
5050
+
5051
+ const argument = node.argument;
5052
+ const args = [b.thunk(argument)];
5053
+
5054
+ if (argument.type === 'Identifier' || argument.type === 'MemberExpression') {
5055
+ args.push(
5056
+ b.arrow(
5057
+ [b.id('v')],
5058
+ /** @type {any} */ ({
5059
+ type: 'AssignmentExpression',
5060
+ operator: '=',
5061
+ left: clone_expression_node(argument, false),
5062
+ right: b.id('v'),
5063
+ metadata: { path: [] },
5064
+ }),
5065
+ ),
5066
+ );
5067
+ }
5068
+
5069
+ return b.call(CREATE_REF_PROP_INTERNAL_NAME, ...args);
5070
+ }
5071
+
4607
5072
  /**
4608
5073
  * @param {any} node
4609
5074
  * @param {TransformContext} transform_context