@tsrx/core 0.0.15 → 0.0.17

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.15",
6
+ "version": "0.0.17",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -137,7 +137,10 @@ export { sanitize_template_string as sanitizeTemplateString } from './utils/sani
137
137
  export { escape } from './utils/escaping.js';
138
138
 
139
139
  // Transform
140
- export { createJsxTransform } from './transform/jsx/index.js';
140
+ export {
141
+ createJsxTransform,
142
+ component_to_function_declaration as componentToFunctionDeclaration,
143
+ } from './transform/jsx/index.js';
141
144
  export {
142
145
  ensure_function_metadata as ensureFunctionMetadata,
143
146
  in_jsx_child_context as inJsxChildContext,
package/src/plugin.js CHANGED
@@ -196,6 +196,79 @@ export function TSRXPlugin(config) {
196
196
  this.#filename = tsrx_options?.filename || null;
197
197
  }
198
198
 
199
+ #previousNonWhitespaceChar() {
200
+ let index = this.pos - 1;
201
+ while (index >= 0) {
202
+ const ch = this.input.charCodeAt(index);
203
+ if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) {
204
+ return ch;
205
+ }
206
+ index--;
207
+ }
208
+ return null;
209
+ }
210
+
211
+ #isDoubleQuotedTextChildStart() {
212
+ if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
213
+ return false;
214
+ }
215
+
216
+ const parent = this.#path.at(-1);
217
+ if (!parent || (parent.type !== 'Component' && parent.type !== 'Element')) {
218
+ return false;
219
+ }
220
+
221
+ const context = this.curContext();
222
+ if (context === tstc.tc_oTag || context === tstc.tc_cTag) {
223
+ return false;
224
+ }
225
+
226
+ const prev = this.#previousNonWhitespaceChar();
227
+ return (
228
+ prev === null ||
229
+ prev === 34 || // "
230
+ prev === 59 || // ;
231
+ prev === 62 || // >
232
+ prev === 123 || // {
233
+ prev === 125 // }
234
+ );
235
+ }
236
+
237
+ #readDoubleQuotedTextChildToken() {
238
+ const start = this.pos;
239
+ let out = '';
240
+ this.pos++;
241
+ let chunkStart = this.pos;
242
+
243
+ while (this.pos < this.input.length) {
244
+ const ch = this.input.charCodeAt(this.pos);
245
+
246
+ if (ch === 34 /* " */) {
247
+ out += this.input.slice(chunkStart, this.pos);
248
+ this.pos++;
249
+ return this.finishToken(tt.string, out);
250
+ }
251
+
252
+ if (ch === 38 /* & */) {
253
+ out += this.input.slice(chunkStart, this.pos);
254
+ out += this.jsx_readEntity();
255
+ chunkStart = this.pos;
256
+ continue;
257
+ }
258
+
259
+ if (acorn.isNewLine(ch)) {
260
+ out += this.input.slice(chunkStart, this.pos);
261
+ out += this.jsx_readNewLine(true);
262
+ chunkStart = this.pos;
263
+ continue;
264
+ }
265
+
266
+ this.pos++;
267
+ }
268
+
269
+ this.raise(start, 'Unterminated double-quoted text child');
270
+ }
271
+
199
272
  /**
200
273
  * @param {number} position
201
274
  * @param {number} end
@@ -559,6 +632,10 @@ export function TSRXPlugin(config) {
559
632
  * @type {Parse.Parser['getTokenFromCode']}
560
633
  */
561
634
  getTokenFromCode(code) {
635
+ if (code === 34 && this.#isDoubleQuotedTextChildStart()) {
636
+ return this.#readDoubleQuotedTextChildToken();
637
+ }
638
+
562
639
  if (code !== 60) {
563
640
  this.#allowTagStartAfterDoubleQuotedText = false;
564
641
  }
@@ -1318,12 +1395,12 @@ export function TSRXPlugin(config) {
1318
1395
  parseDoubleQuotedTextChild() {
1319
1396
  const node = /** @type {AST.TextNode} */ (this.startNode());
1320
1397
  const expression = /** @type {AST.Literal} */ (this.startNode());
1321
- const raw = this.input.slice(this.start, this.end);
1398
+ node.raw = this.input.slice(this.start, this.end);
1322
1399
  const end = this.end;
1323
1400
  const endLoc = this.endLoc;
1324
1401
 
1325
1402
  expression.value = this.value;
1326
- expression.raw = raw;
1403
+ expression.raw = JSON.stringify(this.value);
1327
1404
  node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
1328
1405
 
1329
1406
  this.#allowTagStartAfterDoubleQuotedText = true;
@@ -123,6 +123,69 @@ export function tsx_with_ts_locations() {
123
123
  context.write(': ');
124
124
  context.visit(node.elementType);
125
125
  },
126
+ // esrap's Property printer for method shorthand (`{ foo<T>() {} }`)
127
+ // does not visit `value.typeParameters`, so the `<T>` is dropped from
128
+ // the output and segments.js can't resolve the TSTypeParameterDeclaration's
129
+ // source position. Override only the actual method-shorthand branch —
130
+ // `{ foo: function() {} }` (`node.method === false`) and getters/setters
131
+ // must fall through to base.Property to preserve their printed form.
132
+ Property: (node, context) => {
133
+ if (!node.method || node.value.type !== 'FunctionExpression') {
134
+ base.Property(node, context);
135
+ return;
136
+ }
137
+ const value = node.value;
138
+ if (value.async) context.write('async ');
139
+ if (value.generator) context.write('*');
140
+ if (node.computed) context.write('[');
141
+ context.visit(node.key);
142
+ if (node.computed) context.write(']');
143
+ if (value.typeParameters) {
144
+ context.visit(value.typeParameters);
145
+ }
146
+ context.write('(');
147
+ for (let i = 0; i < value.params.length; i++) {
148
+ if (i > 0) context.write(', ');
149
+ context.visit(value.params[i]);
150
+ }
151
+ context.write(')');
152
+ if (value.returnType) context.visit(value.returnType);
153
+ context.write(' ');
154
+ context.visit(value.body);
155
+ },
156
+ // esrap's ArrowFunctionExpression printer ignores `typeParameters` and
157
+ // `returnType`, so an annotated arrow like `(): Record<...> => ...`
158
+ // prints as `() => ...` and segments.js can't resolve the return-type
159
+ // nodes' positions in the generated output.
160
+ ArrowFunctionExpression: (node, context) => {
161
+ if (node.async) context.write('async ');
162
+ if (node.typeParameters) {
163
+ context.visit(node.typeParameters);
164
+ }
165
+ context.write('(');
166
+ for (let i = 0; i < node.params.length; i++) {
167
+ if (i > 0) context.write(', ');
168
+ context.visit(node.params[i]);
169
+ }
170
+ context.write(')');
171
+ if (node.returnType) {
172
+ context.visit(node.returnType);
173
+ }
174
+ context.write(' => ');
175
+ const body = node.body;
176
+ const wrap_body =
177
+ body.type === 'ObjectExpression' ||
178
+ (body.type === 'AssignmentExpression' && body.left.type === 'ObjectPattern') ||
179
+ (body.type === 'LogicalExpression' && body.left.type === 'ObjectExpression') ||
180
+ (body.type === 'ConditionalExpression' && body.test.type === 'ObjectExpression');
181
+ if (wrap_body) {
182
+ context.write('(');
183
+ context.visit(body);
184
+ context.write(')');
185
+ } else {
186
+ context.visit(body);
187
+ }
188
+ },
126
189
  };
127
190
  for (const type of [
128
191
  // JS nodes whose esrap printer emits no location marker, causing
@@ -138,6 +201,7 @@ export function tsx_with_ts_locations() {
138
201
  'ForOfStatement',
139
202
  'TemplateLiteral',
140
203
  'AwaitExpression',
204
+ 'SwitchStatement',
141
205
  'TaggedTemplateExpression',
142
206
  // JSX wrapper nodes: esrap writes `<`, `>`, `</`, `{`, `}` without
143
207
  // locations, so the opening/closing element's and expression
@@ -54,6 +54,7 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
54
54
  * available_bindings: Map<string, AST.Identifier>,
55
55
  * lazy_next_id: number,
56
56
  * current_css_hash: string | null,
57
+ * inside_element_child?: boolean,
57
58
  * }} TransformContext
58
59
  */
59
60
 
@@ -186,7 +187,9 @@ export function createJsxTransform(platform) {
186
187
  // the names their body actually references before generating props.
187
188
  const body_bindings = collect_param_bindings(as_any.params || []);
188
189
  const body = as_any.body || [];
189
- for (let i = 0; i < body.length; i += 1) {
190
+ const split_index = find_hook_safe_split_index(body, state);
191
+ const collect_end = split_index === -1 ? body.length : split_index;
192
+ for (let i = 0; i < collect_end; i += 1) {
190
193
  collect_statement_bindings(body[i], body_bindings);
191
194
  }
192
195
  state.available_bindings = body_bindings;
@@ -223,7 +226,7 @@ export function createJsxTransform(platform) {
223
226
  const inner = /** @type {any} */ (next() ?? node);
224
227
  const hook = platform.hooks?.transformElement;
225
228
  if (hook) return /** @type {any} */ (hook(inner, state, raw_children));
226
- return /** @type {any} */ (to_jsx_element(inner, state));
229
+ return /** @type {any} */ (to_jsx_element(inner, state, raw_children));
227
230
  },
228
231
 
229
232
  Text(node, { next }) {
@@ -326,12 +329,18 @@ function has_use_server_directive(program) {
326
329
  }
327
330
 
328
331
  /**
332
+ * Lower a TSRX `Component` node into the shared function-declaration form used
333
+ * by the default JSX targets. Platform hooks can reuse this helper and wrap the
334
+ * resulting function in another declaration shape without reimplementing
335
+ * component body lowering, lazy destructuring, helper generation, or top-level
336
+ * await handling.
337
+ *
329
338
  * @param {any} component
330
339
  * @param {TransformContext} transform_context
331
340
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
332
341
  * @returns {AST.FunctionDeclaration}
333
342
  */
334
- function component_to_function_declaration(component, transform_context, walk_helper_state) {
343
+ export function component_to_function_declaration(component, transform_context, walk_helper_state) {
335
344
  const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
336
345
  const params = component.params || [];
337
346
  const body = /** @type {any[]} */ (component.body || []);
@@ -443,10 +452,31 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
443
452
  }
444
453
 
445
454
  if (is_returning_if_statement(child)) {
446
- const branch_has_hooks = body_contains_top_level_hook_call(get_if_consequent_body(child));
447
- const continuation_has_hooks = body_contains_top_level_hook_call(body_nodes.slice(i + 1));
455
+ const branch_has_hooks = body_contains_top_level_hook_call(
456
+ get_if_consequent_body(child),
457
+ transform_context,
458
+ true,
459
+ );
460
+ const continuation_has_hooks = body_contains_top_level_hook_call(
461
+ body_nodes.slice(i + 1),
462
+ transform_context,
463
+ true,
464
+ );
448
465
 
449
466
  if (branch_has_hooks || continuation_has_hooks) {
467
+ if (transform_context.platform.hooks?.isTopLevelSetupCall) {
468
+ statements.push(
469
+ ...create_setup_once_helper_split_returning_if_statements(
470
+ child,
471
+ body_nodes.slice(i + 1),
472
+ render_nodes,
473
+ transform_context,
474
+ ),
475
+ );
476
+ transform_context.available_bindings = saved_bindings;
477
+ return statements;
478
+ }
479
+
450
480
  statements.push(
451
481
  ...create_component_helper_split_returning_if_statements(
452
482
  child,
@@ -460,6 +490,83 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
460
490
  }
461
491
 
462
492
  if (is_lone_return_if_statement(child)) {
493
+ // On platforms where setup runs once (Vue Vapor), an early
494
+ // `if (cond) return;` placed at setup level is non-reactive:
495
+ // `cond` is evaluated only when setup runs and never again.
496
+ // Inline the rest of the body as a render-time ternary so the
497
+ // conditional re-evaluates when `cond` changes after mount.
498
+ // React/Preact/Solid re-run the component body on every render,
499
+ // so the old setup-time early return is already reactive there
500
+ // and we keep it to avoid gratuitous output changes.
501
+ if (transform_context.platform.hooks?.isTopLevelSetupCall) {
502
+ const continuation_body = body_nodes.slice(i + 1);
503
+
504
+ // Render-time inlining unconditionally lifts continuation
505
+ // statements (provide/watch/declarations/etc.) into the
506
+ // parent setup, which would run them regardless of the
507
+ // early-return condition — wrong when the user wrote them
508
+ // after `if (cond) return;`. Fall back to helper-split if
509
+ // the continuation has any non-render statements so they
510
+ // stay scoped to the helper's lifecycle.
511
+ const continuation_has_setup_statements = continuation_body.some(
512
+ (node) =>
513
+ !is_bare_return_statement(node) &&
514
+ !is_returning_if_statement(node) &&
515
+ !is_jsx_child(node),
516
+ );
517
+
518
+ if (continuation_has_setup_statements) {
519
+ statements.push(
520
+ ...create_setup_once_helper_split_returning_if_statements(
521
+ child,
522
+ continuation_body,
523
+ render_nodes,
524
+ transform_context,
525
+ ),
526
+ );
527
+ transform_context.available_bindings = saved_bindings;
528
+ return statements;
529
+ }
530
+
531
+ const continuation_statements = build_render_statements(
532
+ continuation_body,
533
+ false,
534
+ transform_context,
535
+ );
536
+
537
+ for (const stmt of continuation_statements) {
538
+ if (stmt.type === 'ReturnStatement') {
539
+ if (stmt.argument) {
540
+ render_nodes.push(
541
+ /** @type {any} */ ({
542
+ type: 'JSXExpressionContainer',
543
+ expression: set_loc(
544
+ /** @type {any} */ ({
545
+ type: 'ConditionalExpression',
546
+ test: clone_expression_node(child.test),
547
+ consequent: {
548
+ type: 'Literal',
549
+ value: null,
550
+ raw: 'null',
551
+ metadata: { path: [] },
552
+ },
553
+ alternate: stmt.argument,
554
+ metadata: { path: [] },
555
+ }),
556
+ child,
557
+ ),
558
+ metadata: { path: [] },
559
+ }),
560
+ );
561
+ }
562
+ } else {
563
+ statements.push(stmt);
564
+ }
565
+ }
566
+
567
+ break;
568
+ }
569
+
463
570
  statements.push(create_component_lone_return_if_statement(child, render_nodes));
464
571
  continue;
465
572
  }
@@ -519,26 +626,62 @@ function is_interleaved_body(body_nodes) {
519
626
 
520
627
  /**
521
628
  * @param {any[]} body_nodes
629
+ * @param {TransformContext} transform_context
630
+ * @returns {number}
631
+ */
632
+ function find_hook_safe_split_index(body_nodes, transform_context) {
633
+ for (let i = 0; i < body_nodes.length; i += 1) {
634
+ if (!is_lone_return_if_statement(body_nodes[i])) {
635
+ continue;
636
+ }
637
+
638
+ if (body_contains_top_level_hook_call(body_nodes.slice(i + 1), transform_context, true)) {
639
+ return i;
640
+ }
641
+ }
642
+
643
+ return -1;
644
+ }
645
+
646
+ /**
647
+ * @param {any[]} body_nodes
648
+ * @param {TransformContext} transform_context
649
+ * @param {boolean} include_platform_setup
522
650
  * @returns {boolean}
523
651
  */
524
- function body_contains_top_level_hook_call(body_nodes) {
525
- return body_nodes.some(statement_contains_top_level_hook_call);
652
+ function body_contains_top_level_hook_call(
653
+ body_nodes,
654
+ transform_context,
655
+ include_platform_setup = false,
656
+ ) {
657
+ return body_nodes.some((node) =>
658
+ statement_contains_top_level_hook_call(node, transform_context, include_platform_setup),
659
+ );
526
660
  }
527
661
 
528
662
  /**
529
663
  * @param {any} node
664
+ * @param {TransformContext} transform_context
665
+ * @param {boolean} include_platform_setup
530
666
  * @returns {boolean}
531
667
  */
532
- function statement_contains_top_level_hook_call(node) {
533
- return node_contains_top_level_hook_call(node, false);
668
+ function statement_contains_top_level_hook_call(node, transform_context, include_platform_setup) {
669
+ return node_contains_top_level_hook_call(node, false, transform_context, include_platform_setup);
534
670
  }
535
671
 
536
672
  /**
537
673
  * @param {any} node
538
674
  * @param {boolean} inside_nested_function
675
+ * @param {TransformContext} transform_context
676
+ * @param {boolean} include_platform_setup
539
677
  * @returns {boolean}
540
678
  */
541
- function node_contains_top_level_hook_call(node, inside_nested_function) {
679
+ function node_contains_top_level_hook_call(
680
+ node,
681
+ inside_nested_function,
682
+ transform_context,
683
+ include_platform_setup,
684
+ ) {
542
685
  if (!node || typeof node !== 'object') {
543
686
  return false;
544
687
  }
@@ -562,26 +705,53 @@ function node_contains_top_level_hook_call(node, inside_nested_function) {
562
705
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
563
706
  continue;
564
707
  }
565
- if (node_contains_top_level_hook_call(node[key], next_inside_nested_function)) {
708
+ if (
709
+ node_contains_top_level_hook_call(
710
+ node[key],
711
+ next_inside_nested_function,
712
+ transform_context,
713
+ include_platform_setup,
714
+ )
715
+ ) {
566
716
  return true;
567
717
  }
568
718
  }
569
719
  return false;
570
720
  }
571
721
 
572
- if (!inside_nested_function && node.type === 'CallExpression' && is_hook_callee(node.callee)) {
722
+ if (
723
+ !inside_nested_function &&
724
+ node.type === 'CallExpression' &&
725
+ (is_hook_callee(node.callee) ||
726
+ (include_platform_setup &&
727
+ transform_context.platform.hooks?.isTopLevelSetupCall?.(node, transform_context) === true))
728
+ ) {
573
729
  return true;
574
730
  }
575
731
 
576
732
  if (Array.isArray(node)) {
577
- return node.some((child) => node_contains_top_level_hook_call(child, inside_nested_function));
733
+ return node.some((child) =>
734
+ node_contains_top_level_hook_call(
735
+ child,
736
+ inside_nested_function,
737
+ transform_context,
738
+ include_platform_setup,
739
+ ),
740
+ );
578
741
  }
579
742
 
580
743
  for (const key of Object.keys(node)) {
581
744
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
582
745
  continue;
583
746
  }
584
- if (node_contains_top_level_hook_call(node[key], inside_nested_function)) {
747
+ if (
748
+ node_contains_top_level_hook_call(
749
+ node[key],
750
+ inside_nested_function,
751
+ transform_context,
752
+ include_platform_setup,
753
+ )
754
+ ) {
585
755
  return true;
586
756
  }
587
757
  }
@@ -863,6 +1033,12 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
863
1033
  const node = render_nodes[i];
864
1034
  if (node.type !== 'JSXElement') continue;
865
1035
  if (!is_hoist_safe_jsx_node(node)) continue;
1036
+ if (
1037
+ transform_context.platform.hooks?.canHoistStaticNode &&
1038
+ !transform_context.platform.hooks.canHoistStaticNode(node, transform_context)
1039
+ ) {
1040
+ continue;
1041
+ }
866
1042
  if (references_scope_bindings(node, transform_context.available_bindings)) continue;
867
1043
 
868
1044
  const name = create_helper_name(transform_context.helper_state, 'static');
@@ -894,26 +1070,11 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
894
1070
  */
895
1071
  function expand_component_helpers(program) {
896
1072
  program.body = program.body.flatMap((statement) => {
897
- if (statement.type === 'FunctionDeclaration') {
898
- const meta = /** @type {any} */ (statement.metadata);
899
- const statics = meta?.generated_statics || [];
900
- const helpers = meta?.generated_helpers || [];
901
- if (statics.length || helpers.length) {
902
- return [...statics, ...helpers, statement];
903
- }
904
- }
905
-
906
- if (
907
- (statement.type === 'ExportNamedDeclaration' ||
908
- statement.type === 'ExportDefaultDeclaration') &&
909
- statement.declaration?.type === 'FunctionDeclaration'
910
- ) {
911
- const meta = /** @type {any} */ (statement.declaration.metadata);
912
- const statics = meta?.generated_statics || [];
913
- const helpers = meta?.generated_helpers || [];
914
- if (statics.length || helpers.length) {
915
- return [...statics, ...helpers, statement];
916
- }
1073
+ const meta = get_generated_component_metadata(statement);
1074
+ const statics = meta?.generated_statics || [];
1075
+ const helpers = meta?.generated_helpers || [];
1076
+ if (statics.length || helpers.length) {
1077
+ return [...statics, ...helpers, statement];
917
1078
  }
918
1079
 
919
1080
  return [statement];
@@ -922,6 +1083,34 @@ function expand_component_helpers(program) {
922
1083
  return program;
923
1084
  }
924
1085
 
1086
+ /**
1087
+ * Component hooks may replace a `Component` node with a function declaration,
1088
+ * variable declaration, or export-safe expression. Generated helper/statics
1089
+ * metadata is carried on whichever replacement node the hook returns, so
1090
+ * helper expansion must read metadata from that broader set.
1091
+ *
1092
+ * @param {any} node
1093
+ * @returns {{ generated_helpers?: any[], generated_statics?: any[] } | null}
1094
+ */
1095
+ function get_generated_component_metadata(node) {
1096
+ if (!node || typeof node !== 'object') {
1097
+ return null;
1098
+ }
1099
+
1100
+ if (node.metadata?.generated_helpers || node.metadata?.generated_statics) {
1101
+ return node.metadata;
1102
+ }
1103
+
1104
+ if (
1105
+ (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') &&
1106
+ node.declaration?.metadata
1107
+ ) {
1108
+ return node.declaration.metadata;
1109
+ }
1110
+
1111
+ return null;
1112
+ }
1113
+
925
1114
  /**
926
1115
  * @param {any} node
927
1116
  * @returns {boolean}
@@ -1119,6 +1308,53 @@ function create_component_helper_split_returning_if_statements(
1119
1308
  ];
1120
1309
  }
1121
1310
 
1311
+ /**
1312
+ * @param {any} node
1313
+ * @param {any[]} continuation_body
1314
+ * @param {any[]} render_nodes
1315
+ * @param {TransformContext} transform_context
1316
+ * @returns {any[]}
1317
+ */
1318
+ function create_setup_once_helper_split_returning_if_statements(
1319
+ node,
1320
+ continuation_body,
1321
+ render_nodes,
1322
+ transform_context,
1323
+ ) {
1324
+ const consequent_body = get_if_consequent_body(node);
1325
+ const return_index = consequent_body.findIndex(is_bare_return_statement);
1326
+ const branch_body =
1327
+ return_index === -1 ? consequent_body : consequent_body.slice(0, return_index);
1328
+ const branch_helper = branch_body.length
1329
+ ? create_hook_safe_helper(branch_body, undefined, node.consequent, transform_context)
1330
+ : { setup_statements: [], component_element: create_null_literal() };
1331
+ const continuation_helper = continuation_body.length
1332
+ ? create_hook_safe_helper(continuation_body, undefined, node, transform_context)
1333
+ : { setup_statements: [], component_element: create_null_literal() };
1334
+
1335
+ return [
1336
+ ...branch_helper.setup_statements,
1337
+ ...continuation_helper.setup_statements,
1338
+ {
1339
+ type: 'ReturnStatement',
1340
+ argument: combine_render_return_argument(
1341
+ render_nodes,
1342
+ set_loc(
1343
+ /** @type {any} */ ({
1344
+ type: 'ConditionalExpression',
1345
+ test: node.test,
1346
+ consequent: branch_helper.component_element,
1347
+ alternate: continuation_helper.component_element,
1348
+ metadata: { path: [] },
1349
+ }),
1350
+ node,
1351
+ ),
1352
+ ),
1353
+ metadata: { path: [] },
1354
+ },
1355
+ ];
1356
+ }
1357
+
1122
1358
  /**
1123
1359
  * @param {any[]} statements
1124
1360
  * @param {any[]} render_nodes
@@ -1244,13 +1480,8 @@ const TEMPLATE_FRAGMENT_ERROR =
1244
1480
  * @param {TransformContext} transform_context
1245
1481
  * @returns {any}
1246
1482
  */
1247
- function to_jsx_element(node, transform_context) {
1483
+ function to_jsx_element(node, transform_context, raw_children = node.children || []) {
1248
1484
  if (node.type === 'JSXElement') return node;
1249
- if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
1250
- throw new Error(
1251
- `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
1252
- );
1253
- }
1254
1485
  if (!node.id) {
1255
1486
  throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
1256
1487
  }
@@ -1264,8 +1495,30 @@ function to_jsx_element(node, transform_context) {
1264
1495
  transform_context,
1265
1496
  node,
1266
1497
  );
1267
- const selfClosing = !!node.selfClosing;
1268
- const children = create_element_children(node.children || [], transform_context);
1498
+ const walked_children = node.children || [];
1499
+ let selfClosing = !!node.selfClosing;
1500
+ let children;
1501
+ const child_transform = transform_context.platform.hooks?.transformElementChildren?.(
1502
+ node,
1503
+ walked_children,
1504
+ raw_children,
1505
+ attributes,
1506
+ transform_context,
1507
+ );
1508
+
1509
+ if (child_transform) {
1510
+ children = child_transform.children;
1511
+ if (typeof child_transform.selfClosing === 'boolean') {
1512
+ selfClosing = child_transform.selfClosing;
1513
+ }
1514
+ } else {
1515
+ if (walked_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
1516
+ throw new Error(
1517
+ `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
1518
+ );
1519
+ }
1520
+ children = create_element_children(walked_children, transform_context);
1521
+ }
1269
1522
  const has_unmappable_attribute = attributes.some(
1270
1523
  (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
1271
1524
  );
@@ -1325,10 +1578,22 @@ function create_element_children(children, transform_context) {
1325
1578
  }
1326
1579
 
1327
1580
  if (children.every(is_inline_element_child) && !children_contain_return_semantics(children)) {
1328
- return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
1581
+ const saved_inside_element_child = transform_context.inside_element_child;
1582
+ transform_context.inside_element_child = true;
1583
+ try {
1584
+ return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
1585
+ } finally {
1586
+ transform_context.inside_element_child = saved_inside_element_child;
1587
+ }
1329
1588
  }
1330
1589
 
1331
- return [statement_body_to_jsx_child(children, transform_context)];
1590
+ const saved_inside_element_child = transform_context.inside_element_child;
1591
+ transform_context.inside_element_child = true;
1592
+ try {
1593
+ return [statement_body_to_jsx_child(children, transform_context)];
1594
+ } finally {
1595
+ transform_context.inside_element_child = saved_inside_element_child;
1596
+ }
1332
1597
  }
1333
1598
 
1334
1599
  /**
@@ -1391,7 +1656,7 @@ function is_inline_element_child(node) {
1391
1656
  * @returns {ESTreeJSX.JSXExpressionContainer}
1392
1657
  */
1393
1658
  function statement_body_to_jsx_child(body_nodes, transform_context) {
1394
- if (body_contains_top_level_hook_call(body_nodes)) {
1659
+ if (body_contains_top_level_hook_call(body_nodes, transform_context, true)) {
1395
1660
  return hook_safe_statement_body_to_jsx_child(body_nodes, transform_context);
1396
1661
  }
1397
1662
 
@@ -1570,7 +1835,7 @@ function create_hook_safe_helper(body_nodes, key_expression, source_node, transf
1570
1835
  return {
1571
1836
  setup_statements: [
1572
1837
  ...aliases.map((alias) => alias.declaration),
1573
- create_helper_function_declaration_from_expression(helper_id, helper_fn),
1838
+ create_helper_declaration(helper_id, helper_fn, source_node, transform_context),
1574
1839
  ],
1575
1840
  component_element,
1576
1841
  };
@@ -1584,12 +1849,54 @@ function create_hook_safe_helper(body_nodes, key_expression, source_node, transf
1584
1849
  return {
1585
1850
  setup_statements: [
1586
1851
  ...aliases.map((alias) => alias.declaration),
1587
- create_cached_helper_declaration(helper_id, cache_id, helper_fn),
1852
+ create_cached_helper_declaration(
1853
+ helper_id,
1854
+ cache_id,
1855
+ create_helper_init_expression(helper_id, helper_fn, source_node, transform_context),
1856
+ ),
1588
1857
  ],
1589
1858
  component_element,
1590
1859
  };
1591
1860
  }
1592
1861
 
1862
+ /**
1863
+ * @param {AST.Identifier} helper_id
1864
+ * @param {any} helper_fn
1865
+ * @param {any} source_node
1866
+ * @param {TransformContext} transform_context
1867
+ * @returns {any}
1868
+ */
1869
+ function create_helper_declaration(helper_id, helper_fn, source_node, transform_context) {
1870
+ const declaration = create_helper_function_declaration_from_expression(helper_id, helper_fn);
1871
+ const hook = transform_context.platform.hooks?.wrapHelperComponent;
1872
+ return hook ? hook(declaration, helper_id, transform_context, source_node) : declaration;
1873
+ }
1874
+
1875
+ /**
1876
+ * @param {AST.Identifier} helper_id
1877
+ * @param {any} helper_fn
1878
+ * @param {any} source_node
1879
+ * @param {TransformContext} transform_context
1880
+ * @returns {any}
1881
+ */
1882
+ function create_helper_init_expression(helper_id, helper_fn, source_node, transform_context) {
1883
+ const hook = transform_context.platform.hooks?.wrapHelperComponent;
1884
+ if (!hook) return helper_fn;
1885
+
1886
+ const declaration = hook(
1887
+ create_helper_function_declaration_from_expression(helper_id, helper_fn),
1888
+ helper_id,
1889
+ transform_context,
1890
+ source_node,
1891
+ );
1892
+ if (declaration?.type === 'VariableDeclaration') {
1893
+ const init = declaration.declarations?.[0]?.init;
1894
+ if (init) return init;
1895
+ }
1896
+
1897
+ return helper_fn;
1898
+ }
1899
+
1593
1900
  /**
1594
1901
  * @param {any[]} setup_statements
1595
1902
  * @param {ESTreeJSX.JSXElement} component_element
@@ -1723,10 +2030,10 @@ function create_helper_cache_declaration(cache_id) {
1723
2030
  /**
1724
2031
  * @param {AST.Identifier} helper_id
1725
2032
  * @param {AST.Identifier} cache_id
1726
- * @param {any} helper_fn
2033
+ * @param {any} helper_init
1727
2034
  * @returns {any}
1728
2035
  */
1729
- function create_cached_helper_declaration(helper_id, cache_id, helper_fn) {
2036
+ function create_cached_helper_declaration(helper_id, cache_id, helper_init) {
1730
2037
  return /** @type {any} */ ({
1731
2038
  type: 'VariableDeclaration',
1732
2039
  kind: 'const',
@@ -1742,7 +2049,7 @@ function create_cached_helper_declaration(helper_id, cache_id, helper_fn) {
1742
2049
  type: 'AssignmentExpression',
1743
2050
  operator: '=',
1744
2051
  left: clone_identifier(cache_id),
1745
- right: helper_fn,
2052
+ right: helper_init,
1746
2053
  metadata: { path: [] },
1747
2054
  },
1748
2055
  metadata: { path: [] },
@@ -1841,6 +2148,12 @@ function to_jsx_child(node, transform_context) {
1841
2148
  * @returns {ESTreeJSX.JSXExpressionContainer}
1842
2149
  */
1843
2150
  function if_statement_to_jsx_child(node, transform_context) {
2151
+ const render_if_statement = create_render_if_statement(node, transform_context);
2152
+ const conditional_expression = render_if_statement_to_conditional_expression(render_if_statement);
2153
+ if (conditional_expression) {
2154
+ return to_jsx_expression_container(conditional_expression, node);
2155
+ }
2156
+
1844
2157
  return to_jsx_expression_container(
1845
2158
  /** @type {any} */ ({
1846
2159
  type: 'CallExpression',
@@ -1849,10 +2162,7 @@ function if_statement_to_jsx_child(node, transform_context) {
1849
2162
  params: [],
1850
2163
  body: /** @type {any} */ ({
1851
2164
  type: 'BlockStatement',
1852
- body: [
1853
- create_render_if_statement(node, transform_context),
1854
- create_null_return_statement(),
1855
- ],
2165
+ body: [render_if_statement, create_null_return_statement()],
1856
2166
  metadata: { path: [] },
1857
2167
  }),
1858
2168
  async: false,
@@ -1867,6 +2177,61 @@ function if_statement_to_jsx_child(node, transform_context) {
1867
2177
  );
1868
2178
  }
1869
2179
 
2180
+ /**
2181
+ * @param {any} node
2182
+ * @returns {any | null}
2183
+ */
2184
+ function render_if_statement_to_conditional_expression(node) {
2185
+ if (!node || node.type !== 'IfStatement') return null;
2186
+
2187
+ const consequent = block_statement_to_return_expression(node.consequent);
2188
+ if (!consequent) return null;
2189
+
2190
+ let alternate = create_null_literal();
2191
+ if (node.alternate) {
2192
+ if (node.alternate.type === 'IfStatement') {
2193
+ alternate = render_if_statement_to_conditional_expression(node.alternate);
2194
+ if (!alternate) return null;
2195
+ } else {
2196
+ alternate = block_statement_to_return_expression(node.alternate);
2197
+ if (!alternate) return null;
2198
+ }
2199
+ }
2200
+
2201
+ return set_loc(
2202
+ /** @type {any} */ ({
2203
+ type: 'ConditionalExpression',
2204
+ test: node.test,
2205
+ consequent,
2206
+ alternate,
2207
+ metadata: { path: [] },
2208
+ }),
2209
+ node,
2210
+ );
2211
+ }
2212
+
2213
+ /**
2214
+ * @param {any} block
2215
+ * @returns {any | null}
2216
+ */
2217
+ function block_statement_to_return_expression(block) {
2218
+ if (!block || block.type !== 'BlockStatement' || block.body.length === 0) {
2219
+ return null;
2220
+ }
2221
+
2222
+ const statement = block.body[block.body.length - 1];
2223
+ if (!statement || statement.type !== 'ReturnStatement') {
2224
+ return null;
2225
+ }
2226
+
2227
+ const argument = statement.argument || create_null_literal();
2228
+ if (block.body.length === 1) {
2229
+ return argument;
2230
+ }
2231
+
2232
+ return create_hook_safe_helper_iife(block.body.slice(0, -1), argument);
2233
+ }
2234
+
1870
2235
  /**
1871
2236
  * Find the first `key` attribute expression in the top-level elements of a body.
1872
2237
  * Used to propagate keys from loop body elements to wrapper components.
@@ -1923,7 +2288,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1923
2288
 
1924
2289
  const loop_params = get_for_of_iteration_params(node.left, node.index);
1925
2290
  const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
1926
- const has_hooks = body_contains_top_level_hook_call(loop_body);
2291
+ const has_hooks = body_contains_top_level_hook_call(loop_body, transform_context, true);
1927
2292
  const body_key_expression = find_key_expression_in_body(loop_body);
1928
2293
  const explicit_key_expression =
1929
2294
  body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
@@ -1955,6 +2320,17 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1955
2320
  apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
1956
2321
  }
1957
2322
 
2323
+ const platform_for_of = transform_context.platform.hooks?.renderForOf?.(
2324
+ node,
2325
+ loop_params,
2326
+ body_statements,
2327
+ transform_context,
2328
+ );
2329
+ if (platform_for_of) {
2330
+ transform_context.available_bindings = saved_bindings;
2331
+ return platform_for_of;
2332
+ }
2333
+
1958
2334
  // Restore bindings
1959
2335
  transform_context.available_bindings = saved_bindings;
1960
2336
 
@@ -2070,7 +2446,7 @@ function switch_statement_to_jsx_child(node, transform_context) {
2070
2446
  * - `pending` → `<Suspense fallback={...}>`
2071
2447
  * - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
2072
2448
  * - both → ErrorBoundary wraps Suspense
2073
- * - `finally` blocks are not supported in component template context
2449
+ * - JavaScript `try/finally` is not part of component template control flow
2074
2450
  *
2075
2451
  * @param {any} node
2076
2452
  * @param {TransformContext} transform_context
@@ -2084,7 +2460,7 @@ function try_statement_to_jsx_child(node, transform_context) {
2084
2460
  if (finalizer) {
2085
2461
  throw create_compile_error(
2086
2462
  finalizer,
2087
- `${transform_context.platform.name} TSRX does not support \`finally\` blocks in component templates. Move the try statement into a function if you need a finally block.`,
2463
+ `${transform_context.platform.name} TSRX does not support JavaScript \`try/finally\` in component templates. \`finally\` is not part of TSRX control flow; move the try/finally into a function if you need cleanup logic.`,
2088
2464
  );
2089
2465
  }
2090
2466
 
@@ -2095,6 +2471,13 @@ function try_statement_to_jsx_child(node, transform_context) {
2095
2471
  );
2096
2472
  }
2097
2473
 
2474
+ if (pending && transform_context.platform.validation.unsupportedTryPendingMessage) {
2475
+ throw create_compile_error(
2476
+ pending,
2477
+ transform_context.platform.validation.unsupportedTryPendingMessage,
2478
+ );
2479
+ }
2480
+
2098
2481
  // Validate that try body contains JSX if pending block is present
2099
2482
  if (pending) {
2100
2483
  const try_body = node.block.body || [];
@@ -2182,6 +2565,54 @@ function try_statement_to_jsx_child(node, transform_context) {
2182
2565
 
2183
2566
  transform_context.available_bindings = saved_catch_bindings;
2184
2567
 
2568
+ const boundary_content =
2569
+ transform_context.platform.hooks?.createErrorBoundaryContent?.(
2570
+ try_content,
2571
+ transform_context,
2572
+ node,
2573
+ ) ?? null;
2574
+
2575
+ if (boundary_content && transform_context.inside_element_child) {
2576
+ result = to_jsx_expression_container(
2577
+ /** @type {any} */ ({
2578
+ type: 'CallExpression',
2579
+ callee: { type: 'Identifier', name: 'TsrxErrorBoundary', metadata: { path: [] } },
2580
+ arguments: [
2581
+ {
2582
+ type: 'ObjectExpression',
2583
+ properties: [
2584
+ {
2585
+ type: 'Property',
2586
+ key: { type: 'Identifier', name: 'fallback', metadata: { path: [] } },
2587
+ value: fallback_fn,
2588
+ kind: 'init',
2589
+ method: false,
2590
+ shorthand: false,
2591
+ computed: false,
2592
+ metadata: { path: [] },
2593
+ },
2594
+ {
2595
+ type: 'Property',
2596
+ key: { type: 'Identifier', name: 'content', metadata: { path: [] } },
2597
+ value: boundary_content,
2598
+ kind: 'init',
2599
+ method: false,
2600
+ shorthand: false,
2601
+ computed: false,
2602
+ metadata: { path: [] },
2603
+ },
2604
+ ],
2605
+ metadata: { path: [] },
2606
+ },
2607
+ ],
2608
+ optional: false,
2609
+ metadata: { path: [] },
2610
+ }),
2611
+ );
2612
+
2613
+ return result;
2614
+ }
2615
+
2185
2616
  result = create_jsx_element(
2186
2617
  'TsrxErrorBoundary',
2187
2618
  [
@@ -2191,8 +2622,18 @@ function try_statement_to_jsx_child(node, transform_context) {
2191
2622
  value: to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
2192
2623
  metadata: { path: [] },
2193
2624
  },
2625
+ ...(boundary_content
2626
+ ? [
2627
+ {
2628
+ type: 'JSXAttribute',
2629
+ name: { type: 'JSXIdentifier', name: 'content', metadata: { path: [] } },
2630
+ value: to_jsx_expression_container(boundary_content),
2631
+ metadata: { path: [] },
2632
+ },
2633
+ ]
2634
+ : []),
2194
2635
  ],
2195
- [result],
2636
+ boundary_content ? [] : [result],
2196
2637
  );
2197
2638
  }
2198
2639
 
@@ -2315,7 +2756,11 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
2315
2756
  function create_render_if_statement(node, transform_context) {
2316
2757
  const consequent_body =
2317
2758
  node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
2318
- const consequent_has_hooks = body_contains_top_level_hook_call(consequent_body);
2759
+ const consequent_has_hooks = body_contains_top_level_hook_call(
2760
+ consequent_body,
2761
+ transform_context,
2762
+ true,
2763
+ );
2319
2764
 
2320
2765
  let alternate = null;
2321
2766
  if (node.alternate) {
@@ -2323,7 +2768,11 @@ function create_render_if_statement(node, transform_context) {
2323
2768
  alternate = create_render_if_statement(node.alternate, transform_context);
2324
2769
  } else {
2325
2770
  const alternate_body = node.alternate.body || [node.alternate];
2326
- const alternate_has_hooks = body_contains_top_level_hook_call(alternate_body);
2771
+ const alternate_has_hooks = body_contains_top_level_hook_call(
2772
+ alternate_body,
2773
+ transform_context,
2774
+ true,
2775
+ );
2327
2776
  alternate = set_loc(
2328
2777
  /** @type {any} */ ({
2329
2778
  type: 'BlockStatement',
@@ -2388,7 +2837,7 @@ function create_render_switch_case(switch_case, transform_context) {
2388
2837
  body_without_break.push(child);
2389
2838
  }
2390
2839
 
2391
- if (body_contains_top_level_hook_call(body_without_break)) {
2840
+ if (body_contains_top_level_hook_call(body_without_break, transform_context, true)) {
2392
2841
  return /** @type {any} */ ({
2393
2842
  type: 'SwitchCase',
2394
2843
  test: switch_case.test,
@@ -2476,6 +2925,10 @@ function to_jsx_expression_container(expression, source_node = expression) {
2476
2925
  * @returns {any[]}
2477
2926
  */
2478
2927
  function transform_element_attributes_dispatch(attrs, transform_context, element) {
2928
+ const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
2929
+ if (preprocess) {
2930
+ attrs = preprocess(attrs, transform_context, element);
2931
+ }
2479
2932
  const hook = transform_context.platform.hooks?.transformElementAttributes;
2480
2933
  if (hook) return hook(attrs, transform_context, element);
2481
2934
  return attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
@@ -500,6 +500,9 @@ export function convert_source_map_to_mappings(
500
500
  node.metadata?.lazy_param_is_component ? replace_label_to_component : undefined,
501
501
  );
502
502
  }
503
+ if ('hover' in (node.metadata || {})) {
504
+ token.metadata.hover = /** @type {any} */ (node.metadata).hover;
505
+ }
503
506
  if (node.metadata?.disable_verification) {
504
507
  token.mappingData = { ...mapping_data, verification: false };
505
508
  }
@@ -1450,6 +1453,12 @@ export function convert_source_map_to_mappings(
1450
1453
  }
1451
1454
  }
1452
1455
 
1456
+ if (node.loc) {
1457
+ mappings.push(
1458
+ get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1459
+ );
1460
+ }
1461
+
1453
1462
  return;
1454
1463
  } else if (node.type === 'SwitchCase') {
1455
1464
  // Visit in source order: test, consequent
@@ -1696,25 +1705,13 @@ export function convert_source_map_to_mappings(
1696
1705
  node.type === 'TSTypeParameterDeclaration'
1697
1706
  ) {
1698
1707
  if (node.loc) {
1699
- // Generic spans can be emitted by downstream transforms with sparse source-map
1700
- // coverage around the angle-bracket delimiters. Skip missing whole-node mappings
1701
- // instead of crashing Volar, and rely on child type-node mappings instead.
1702
- try {
1703
- const mapping = get_mapping_from_node(
1704
- node,
1705
- src_to_gen_map,
1706
- gen_line_offsets,
1707
- mapping_data_verify_only,
1708
- );
1709
- mappings.push(mapping);
1710
- } catch (error) {
1711
- if (
1712
- !(error instanceof Error) ||
1713
- !error.message.startsWith('No source map entry for position')
1714
- ) {
1715
- throw error;
1716
- }
1717
- }
1708
+ const mapping = get_mapping_from_node(
1709
+ node,
1710
+ src_to_gen_map,
1711
+ gen_line_offsets,
1712
+ mapping_data_verify_only,
1713
+ );
1714
+ mappings.push(mapping);
1718
1715
  }
1719
1716
  // Generic type parameters - visit to collect type variable names
1720
1717
  if (node.params) {
@@ -681,14 +681,15 @@ export function prop(kind, key, value, computed = false, shorthand = false) {
681
681
  * @returns {AST.PropertyDefinition}
682
682
  */
683
683
  export function prop_def(key, value, computed = false, is_static = false) {
684
- return {
684
+ return /** @type {AST.PropertyDefinition} */ ({
685
685
  type: 'PropertyDefinition',
686
686
  key,
687
687
  value,
688
+ decorators: [],
688
689
  computed,
689
690
  static: is_static,
690
691
  metadata: { path: [] },
691
- };
692
+ });
692
693
  }
693
694
 
694
695
  /**
@@ -918,15 +919,16 @@ export function for_of(left, right, body, await_flag = false, loc_info) {
918
919
  * @returns {AST.MethodDefinition}
919
920
  */
920
921
  export function method(kind, key, params, body, computed = false, is_static = false) {
921
- return {
922
+ return /** @type {AST.MethodDefinition} */ ({
922
923
  type: 'MethodDefinition',
923
924
  key,
924
925
  kind,
925
926
  value: function_builder(null, params, block(body)),
927
+ decorators: [],
926
928
  computed,
927
929
  static: is_static,
928
930
  metadata: { path: [] },
929
- };
931
+ });
930
932
  }
931
933
 
932
934
  /**
package/types/index.d.ts CHANGED
@@ -10,11 +10,12 @@ import type {
10
10
  JsxPlatformHooks,
11
11
  JsxTransformOptions,
12
12
  JsxTransformResult,
13
+ componentToFunctionDeclaration,
13
14
  createJsxTransform,
14
15
  } from './jsx-platform';
15
16
 
16
17
  export type { Parse, JsxPlatform, JsxPlatformHooks, JsxTransformOptions, JsxTransformResult };
17
- export { createJsxTransform };
18
+ export { createJsxTransform, componentToFunctionDeclaration };
18
19
 
19
20
  /**
20
21
  * Compile error interface
@@ -397,6 +398,7 @@ declare module 'estree' {
397
398
  export interface TextNode extends AST.BaseExpression {
398
399
  type: 'Text';
399
400
  expression: AST.Expression;
401
+ raw?: string;
400
402
  loc?: AST.SourceLocation;
401
403
  }
402
404
 
@@ -56,12 +56,31 @@ export interface JsxPlatformHooks {
56
56
  tryStatement?: (node: any, ctx: any) => any;
57
57
  };
58
58
  /**
59
- * Lower a `component` declaration to a FunctionDeclaration. React / Preact
60
- * use a default that extracts hooks, hoists statics, and handles async
61
- * bodies. Solid replaces this with a setup-once implementation that
62
- * hoists post-early-return JSX into a reactive `<Show>`.
59
+ * Mark a top-level call expression inside a control-flow branch as requiring
60
+ * helper-component isolation so setup/state is created once per mounted
61
+ * branch instead of once per parent rerender. Vue uses this for branch-local
62
+ * Composition API state like `ref()`.
63
+ */
64
+ isTopLevelSetupCall?: (callExpression: any, ctx: any) => boolean;
65
+ /**
66
+ * Lower a `component` declaration to the replacement node for its current
67
+ * position. React / Preact use the default helper and return a
68
+ * `FunctionDeclaration`. Other targets may return a variable declaration or
69
+ * an expression that wraps the shared lowered function body (for example,
70
+ * `defineVaporComponent(...)`).
71
+ *
72
+ * The default lowering is exported as `componentToFunctionDeclaration()` so
73
+ * platform hooks can build on it instead of reimplementing component body
74
+ * handling.
63
75
  */
64
76
  componentToFunction?: (component: any, ctx: any, helperState?: any) => any;
77
+ /**
78
+ * Wrap a hoisted helper component declaration emitted by the shared control-
79
+ * flow splitter. The default is the plain function declaration; Vue uses
80
+ * this to wrap helpers in `defineVaporComponent(...)` so branch-local setup
81
+ * state behaves like normal component state.
82
+ */
83
+ wrapHelperComponent?: (helperFn: any, helperId: any, ctx: any, sourceNode: any) => any;
65
84
  /**
66
85
  * Inject module-level imports after the main walk. Default: import
67
86
  * `Suspense` from `platform.imports.suspense` and `TsrxErrorBoundary`
@@ -76,6 +95,27 @@ export interface JsxPlatformHooks {
76
95
  * attributes through its composite-element handling.
77
96
  */
78
97
  transformElementAttributes?: (attrs: any[], ctx: any, element: any) => any[];
98
+ /**
99
+ * Rewrite or normalize raw Ripple attributes before the shared
100
+ * `to_jsx_attribute()` mapping runs. Targets can use this to merge multiple
101
+ * keyword attributes, such as collapsing repeated `{ref ...}` entries into a
102
+ * single `RefAttribute` backed by an array expression.
103
+ */
104
+ preprocessElementAttributes?: (attrs: any[], ctx: any, element: any) => any[];
105
+ /**
106
+ * Optionally replace the default React-style `.map(...)` lowering for a
107
+ * `for...of` body after the shared transform has already produced its render
108
+ * statements and applied any explicit or implicit keys. Vue uses this to hand
109
+ * the loop to the downstream Vapor JSX compiler as a native `v-for` template.
110
+ */
111
+ renderForOf?: (node: any, loopParams: any[], bodyStatements: any[], ctx: any) => any | null;
112
+ /**
113
+ * Optionally move the primary `try { ... }` render content into an explicit
114
+ * error-boundary prop instead of rendering it as the boundary's JSX children.
115
+ * Vue Vapor uses this because boundary content must execute lazily from a
116
+ * zero-argument function.
117
+ */
118
+ createErrorBoundaryContent?: (tryContent: any, ctx: any, node: any) => any | null;
79
119
  /**
80
120
  * Lower a Ripple `Element` node to a JSXElement. Default is the
81
121
  * factory's `to_jsx_element`. The hook receives the walker-transformed
@@ -85,6 +125,30 @@ export interface JsxPlatformHooks {
85
125
  * generic text→JSXExpressionContainer transform runs.
86
126
  */
87
127
  transformElement?: (inner: any, ctx: any, rawChildren: any[]) => any;
128
+ /**
129
+ * Optionally rewrite a host element's children into attributes or another
130
+ * specialized child shape after generic attribute lowering but before the
131
+ * default child-to-JSX conversion runs.
132
+ *
133
+ * This lets a target support target-native DOM content props such as
134
+ * `textContent` / `innerHTML` without forking the whole element lowering.
135
+ * The hook may mutate `attrs` directly and either return a replacement
136
+ * `children` array (plus optional `selfClosing` override) or `null` to fall
137
+ * back to the default child handling.
138
+ */
139
+ transformElementChildren?: (
140
+ element: any,
141
+ walkedChildren: any[],
142
+ rawChildren: any[],
143
+ attrs: any[],
144
+ ctx: any,
145
+ ) => { children: any[]; selfClosing?: boolean } | null;
146
+ /**
147
+ * Decide whether a JSX subtree may be hoisted to module scope when it is
148
+ * otherwise statically safe. Targets can use this to keep runtime-sensitive
149
+ * JSX, such as component invocations, inside render/setup execution.
150
+ */
151
+ canHoistStaticNode?: (node: any, ctx: any) => boolean;
88
152
  /**
89
153
  * Custom validation for a component body that uses top-level `await`.
90
154
  * Default: enforce `validation.requireUseServerForAwait`. Solid rejects
@@ -167,6 +231,15 @@ export interface JsxPlatform {
167
231
  * Default: `true`.
168
232
  */
169
233
  scanUseServerDirectiveForAwaitWithCustomValidator?: boolean;
234
+ /**
235
+ * Optional branded compiler error for targets that cannot lower
236
+ * `try { ... } pending { ... }` in component template context.
237
+ *
238
+ * When provided, the shared try-block lowering rejects any `pending`
239
+ * block with this message instead of emitting a React-style
240
+ * `<Suspense fallback={...}>` wrapper.
241
+ */
242
+ unsupportedTryPendingMessage?: string;
170
243
  };
171
244
 
172
245
  /**
@@ -191,3 +264,9 @@ export function createJsxTransform(
191
264
  filename?: string,
192
265
  options?: JsxTransformOptions,
193
266
  ) => JsxTransformResult;
267
+
268
+ export function componentToFunctionDeclaration(
269
+ component: any,
270
+ ctx: any,
271
+ helperState?: any,
272
+ ): AST.FunctionDeclaration;