@tsrx/react 0.0.1 → 0.0.3

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": "React compiler built on @tsrx/core",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.1",
6
+ "version": "0.0.3",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** @import * as AST from 'estree' */
2
+ /** @import { CodeMapping, ParseOptions } from '@tsrx/core/types' */
2
3
 
3
4
  import { createVolarMappingsResult, parseModule } from '@tsrx/core';
4
5
  import { transform } from './transform.js';
@@ -7,10 +8,11 @@ import { transform } from './transform.js';
7
8
  * Parse tsrx-react source code to an ESTree AST.
8
9
  * @param {string} source
9
10
  * @param {string} [filename]
11
+ * @param {ParseOptions} [options]
10
12
  * @returns {AST.Program}
11
13
  */
12
- export function parse(source, filename) {
13
- return parseModule(source, filename);
14
+ export function parse(source, filename, options) {
15
+ return parseModule(source, filename, options);
14
16
  }
15
17
 
16
18
  /**
@@ -32,13 +34,13 @@ export function compile(source, filename) {
32
34
  *
33
35
  * @param {string} source
34
36
  * @param {string} [filename]
37
+ * @param {ParseOptions} [options]
35
38
  * @returns {import('@tsrx/core/types').VolarMappingsResult}
36
39
  */
37
- export function compile_to_volar_mappings(source, filename) {
38
- const ast = parseModule(source, filename);
40
+ export function compile_to_volar_mappings(source, filename, options) {
41
+ const ast = parseModule(source, filename, options);
39
42
  const transformed = transform(ast, source, filename);
40
-
41
- return createVolarMappingsResult({
43
+ const result = createVolarMappingsResult({
42
44
  ast: transformed.ast,
43
45
  ast_from_source: ast,
44
46
  source,
@@ -46,4 +48,59 @@ export function compile_to_volar_mappings(source, filename) {
46
48
  source_map: transformed.map,
47
49
  errors: [],
48
50
  });
51
+
52
+ return {
53
+ ...result,
54
+ mappings: dedupe_mappings(result.mappings),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Remove byte-for-byte duplicate mappings. React helper extraction can emit
60
+ * identical mapping entries for the same source and generated span, which
61
+ * causes Volar to merge duplicate hover/navigation results.
62
+ *
63
+ * @param {CodeMapping[]} mappings
64
+ * @returns {CodeMapping[]}
65
+ */
66
+ function dedupe_mappings(mappings) {
67
+ const deduped = [];
68
+ const seen = new Set();
69
+
70
+ for (const mapping of mappings) {
71
+ const key = JSON.stringify(serialize_mapping_value(mapping));
72
+
73
+ if (seen.has(key)) {
74
+ continue;
75
+ }
76
+
77
+ seen.add(key);
78
+ deduped.push(mapping);
79
+ }
80
+
81
+ return deduped;
82
+ }
83
+
84
+ /**
85
+ * @param {unknown} value
86
+ * @returns {unknown}
87
+ */
88
+ function serialize_mapping_value(value) {
89
+ if (typeof value === 'function') {
90
+ return value.toString();
91
+ }
92
+
93
+ if (Array.isArray(value)) {
94
+ return value.map(serialize_mapping_value);
95
+ }
96
+
97
+ if (value && typeof value === 'object') {
98
+ return Object.fromEntries(
99
+ Object.entries(value)
100
+ .sort(([left], [right]) => left.localeCompare(right))
101
+ .map(([key, nested_value]) => [key, serialize_mapping_value(nested_value)]),
102
+ );
103
+ }
104
+
105
+ return value;
49
106
  }
package/src/transform.js CHANGED
@@ -11,9 +11,16 @@ import { renderStylesheets, setLocation } from '@tsrx/core';
11
11
  * local_statement_component_index: number,
12
12
  * needs_error_boundary: boolean,
13
13
  * needs_suspense: boolean,
14
+ * helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
15
+ * available_bindings: Map<string, AST.Identifier>,
16
+ * lazy_next_id: number,
14
17
  * }} TransformContext
15
18
  */
16
19
 
20
+ /**
21
+ * @typedef {{ source_name: string, read: () => any }} LazyBinding
22
+ */
23
+
17
24
  /**
18
25
  * Transform a parsed tsrx-react AST into a TSX/JSX module.
19
26
  *
@@ -39,6 +46,9 @@ export function transform(ast, source, filename) {
39
46
  local_statement_component_index: 0,
40
47
  needs_error_boundary: false,
41
48
  needs_suspense: false,
49
+ helper_state: null,
50
+ available_bindings: new Map(),
51
+ lazy_next_id: 0,
42
52
  };
43
53
 
44
54
  walk(/** @type {any} */ (ast), transform_context, {
@@ -56,8 +66,39 @@ export function transform(ast, source, filename) {
56
66
 
57
67
  const transformed = walk(/** @type {any} */ (ast), transform_context, {
58
68
  Component(node, { next, state }) {
69
+ const as_any = /** @type {any} */ (node);
70
+
71
+ // Set up helper_state and bindings BEFORE next() so that nested
72
+ // hook_safe_* calls (inside Element children) can register helpers
73
+ // and access available bindings during the bottom-up walk.
74
+ const helper_state = create_helper_state(as_any.id?.name || 'Component');
75
+ const saved_helper_state = state.helper_state;
76
+ const saved_bindings = state.available_bindings;
77
+ state.helper_state = helper_state;
78
+
79
+ // Pre-collect component body bindings (params + top-level statements)
80
+ // so that Element children processed during the bottom-up walk can see
81
+ // the full scope. Without this, hoisted helpers would miss body-level
82
+ // variables like `const [x] = useState(...)` and produce ReferenceErrors.
83
+ // Only collect up to the split point — bindings declared after a
84
+ // hook-safe split aren't in scope at the return statement and would
85
+ // cause ReferenceErrors if passed as helper props.
86
+ const body_bindings = collect_param_bindings(as_any.params || []);
87
+ const body = as_any.body || [];
88
+ const split_index = find_hook_safe_split_index(body);
89
+ const collect_end = split_index === -1 ? body.length : split_index;
90
+ for (let i = 0; i < collect_end; i += 1) {
91
+ collect_statement_bindings(body[i], body_bindings);
92
+ }
93
+ state.available_bindings = body_bindings;
94
+
59
95
  const inner = /** @type {any} */ (next() ?? node);
60
- return /** @type {any} */ (component_to_function_declaration(inner, state));
96
+
97
+ // Restore context
98
+ state.helper_state = saved_helper_state;
99
+ state.available_bindings = saved_bindings;
100
+
101
+ return /** @type {any} */ (component_to_function_declaration(inner, state, helper_state));
61
102
  },
62
103
 
63
104
  Tsx(node, { next }) {
@@ -77,7 +118,9 @@ export function transform(ast, source, filename) {
77
118
 
78
119
  Text(node, { next }) {
79
120
  const inner = /** @type {any} */ (next() ?? node);
80
- return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
121
+ return /** @type {any} */ (
122
+ to_jsx_expression_container(to_text_expression(inner.expression, inner), inner)
123
+ );
81
124
  },
82
125
 
83
126
  TSRXExpression(node, { next }) {
@@ -107,37 +150,579 @@ export function transform(ast, source, filename) {
107
150
  return { ast: expanded, code: result.code, map: result.map, css };
108
151
  }
109
152
 
153
+ // --- Lazy destructuring support ---
154
+
155
+ /**
156
+ * Generate a unique lazy identifier name for a lazy destructuring pattern.
157
+ * @param {TransformContext} transform_context
158
+ * @returns {string}
159
+ */
160
+ function generate_lazy_id(transform_context) {
161
+ return `__lazy${transform_context.lazy_next_id++}`;
162
+ }
163
+
164
+ /**
165
+ * Collect lazy bindings from a destructuring pattern.
166
+ * For `&{name, age}`, maps `name` → `source.name`, `age` → `source.age`.
167
+ * For `&[a, b]`, maps `a` → `source[0]`, `b` → `source[1]`.
168
+ * Handles nested AssignmentPattern (default values) and RestElement.
169
+ *
170
+ * @param {any} pattern - The ObjectPattern or ArrayPattern with lazy: true
171
+ * @param {string} source_name - The generated identifier name for the source
172
+ * @param {Map<string, LazyBinding>} lazy_bindings - Map to populate
173
+ */
174
+ function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
175
+ if (pattern.type === 'ObjectPattern') {
176
+ for (const prop of pattern.properties || []) {
177
+ if (prop.type === 'RestElement') {
178
+ // Rest element in object pattern — skip for now (complex to transform)
179
+ continue;
180
+ }
181
+ const value = prop.value;
182
+ const actual = value.type === 'AssignmentPattern' ? value.left : value;
183
+ if (actual.type === 'Identifier') {
184
+ const key = prop.key;
185
+ const computed = prop.computed || key.type !== 'Identifier';
186
+ lazy_bindings.set(actual.name, {
187
+ source_name,
188
+ read: () => ({
189
+ type: 'MemberExpression',
190
+ object: create_generated_identifier(source_name),
191
+ property: computed
192
+ ? { ...key }
193
+ : { type: 'Identifier', name: key.name, metadata: { path: [] } },
194
+ computed,
195
+ optional: false,
196
+ metadata: { path: [] },
197
+ }),
198
+ });
199
+ }
200
+ }
201
+ } else if (pattern.type === 'ArrayPattern') {
202
+ for (let i = 0; i < (pattern.elements || []).length; i++) {
203
+ const element = pattern.elements[i];
204
+ if (!element) continue;
205
+ if (element.type === 'RestElement') {
206
+ // Rest element in array pattern — skip for now
207
+ continue;
208
+ }
209
+ const actual = element.type === 'AssignmentPattern' ? element.left : element;
210
+ if (actual.type === 'Identifier') {
211
+ const index = i;
212
+ lazy_bindings.set(actual.name, {
213
+ source_name,
214
+ read: () => ({
215
+ type: 'MemberExpression',
216
+ object: create_generated_identifier(source_name),
217
+ property: { type: 'Literal', value: index, raw: String(index), metadata: { path: [] } },
218
+ computed: true,
219
+ optional: false,
220
+ metadata: { path: [] },
221
+ }),
222
+ });
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Collect lazy bindings from component params and body variable declarations
230
+ * WITHOUT modifying any AST nodes. Returns a map of binding name → accessor info.
231
+ * Stores the generated identifier name on the pattern's metadata for later replacement.
232
+ *
233
+ * @param {any[]} params - Component params (metadata annotated, not structurally mutated)
234
+ * @param {any[]} body - Component body (metadata annotated, not structurally mutated)
235
+ * @param {TransformContext} transform_context
236
+ * @returns {Map<string, LazyBinding>}
237
+ */
238
+ function collect_lazy_bindings_from_component(params, body, transform_context) {
239
+ /** @type {Map<string, LazyBinding>} */
240
+ const lazy_bindings = new Map();
241
+
242
+ // Collect from lazy params
243
+ for (const param of params) {
244
+ const pattern = param.type === 'AssignmentPattern' ? param.left : param;
245
+
246
+ if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
247
+ const lazy_name = generate_lazy_id(transform_context);
248
+ collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
249
+ pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
250
+ }
251
+ }
252
+
253
+ // Collect from lazy variable declarations in body
254
+ for (const statement of body) {
255
+ if (statement.type !== 'VariableDeclaration') continue;
256
+
257
+ for (const declarator of statement.declarations || []) {
258
+ const pattern = declarator.id;
259
+ if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
260
+ const lazy_name = generate_lazy_id(transform_context);
261
+ collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
262
+ pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
263
+ }
264
+ }
265
+ }
266
+
267
+ return lazy_bindings;
268
+ }
269
+
270
+ /**
271
+ * Walk an AST node tree and replace identifier references that match lazy bindings
272
+ * with their corresponding member expressions (e.g., `name` → `__lazy0.name`).
273
+ * Also handles AssignmentExpression and UpdateExpression targets.
274
+ *
275
+ * @param {any} node - The AST node to walk
276
+ * @param {Map<string, LazyBinding>} lazy_bindings - Map of lazy binding names
277
+ * @returns {any}
278
+ */
279
+ function apply_lazy_transforms(node, lazy_bindings) {
280
+ if (!node || typeof node !== 'object') return node;
281
+ if (Array.isArray(node)) return node.map((child) => apply_lazy_transforms(child, lazy_bindings));
282
+
283
+ // Don't recurse into nested function declarations (helper components have their own scope)
284
+ if (
285
+ node.type === 'FunctionDeclaration' ||
286
+ node.type === 'FunctionExpression' ||
287
+ node.type === 'ArrowFunctionExpression'
288
+ ) {
289
+ // Transform default parameter values (e.g. (step = count) => ...) with the
290
+ // outer lazy_bindings, since defaults are evaluated in the outer scope.
291
+ let params_changed = false;
292
+ const new_params = (node.params || []).map((/** @type {any} */ param) => {
293
+ const transformed = transform_param_defaults(param, lazy_bindings);
294
+ if (transformed !== param) params_changed = true;
295
+ return transformed;
296
+ });
297
+
298
+ // Check if any params shadow a lazy binding — if so, exclude those names
299
+ /** @type {Set<string>} */
300
+ const shadowed = new Set();
301
+ for (const param of node.params || []) {
302
+ collect_shadowed_names(param, lazy_bindings, shadowed);
303
+ }
304
+
305
+ const inner_bindings =
306
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
307
+ if (inner_bindings.size === 0 && !params_changed) return node;
308
+
309
+ const new_body =
310
+ inner_bindings.size > 0 ? apply_lazy_transforms(node.body, inner_bindings) : node.body;
311
+
312
+ if (new_body !== node.body || params_changed) {
313
+ return {
314
+ ...node,
315
+ params: params_changed ? new_params : node.params,
316
+ body: new_body,
317
+ };
318
+ }
319
+ return node;
320
+ }
321
+
322
+ // Handle block-scoped variable shadowing (const/let/var that shadows a lazy name)
323
+ if (node.type === 'BlockStatement' || node.type === 'Program') {
324
+ const block_bindings = collect_block_shadowed_names(node.body, lazy_bindings);
325
+ const effective_bindings =
326
+ block_bindings.size > 0 ? remove_shadowed(lazy_bindings, block_bindings) : lazy_bindings;
327
+ if (effective_bindings.size === 0 && block_bindings.size > 0) return node;
328
+
329
+ let changed = false;
330
+ const new_body = node.body.map((/** @type {any} */ stmt) => {
331
+ const transformed = apply_lazy_transforms(stmt, effective_bindings);
332
+ if (transformed !== stmt) changed = true;
333
+ return transformed;
334
+ });
335
+ return changed ? { ...node, body: new_body } : node;
336
+ }
337
+
338
+ // Handle catch clause parameter shadowing
339
+ if (node.type === 'CatchClause') {
340
+ /** @type {Set<string>} */
341
+ const shadowed = new Set();
342
+ if (node.param) collect_shadowed_names(node.param, lazy_bindings, shadowed);
343
+ const effective_bindings =
344
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
345
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
346
+ if (new_body !== node.body) return { ...node, body: new_body };
347
+ return node;
348
+ }
349
+
350
+ // Handle for-loop variable shadowing
351
+ if (node.type === 'ForStatement') {
352
+ /** @type {Set<string>} */
353
+ const shadowed = new Set();
354
+ if (node.init?.type === 'VariableDeclaration') {
355
+ for (const decl of node.init.declarations) {
356
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
357
+ }
358
+ }
359
+ const effective_bindings =
360
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
361
+ let changed = false;
362
+ const new_init = apply_lazy_transforms(node.init, effective_bindings);
363
+ if (new_init !== node.init) changed = true;
364
+ const new_test = apply_lazy_transforms(node.test, effective_bindings);
365
+ if (new_test !== node.test) changed = true;
366
+ const new_update = apply_lazy_transforms(node.update, effective_bindings);
367
+ if (new_update !== node.update) changed = true;
368
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
369
+ if (new_body !== node.body) changed = true;
370
+ return changed
371
+ ? { ...node, init: new_init, test: new_test, update: new_update, body: new_body }
372
+ : node;
373
+ }
374
+
375
+ if (node.type === 'ForOfStatement' || node.type === 'ForInStatement') {
376
+ /** @type {Set<string>} */
377
+ const shadowed = new Set();
378
+ if (node.left?.type === 'VariableDeclaration') {
379
+ for (const decl of node.left.declarations) {
380
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
381
+ }
382
+ }
383
+ const effective_bindings =
384
+ shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
385
+ let changed = false;
386
+ const new_right = apply_lazy_transforms(node.right, lazy_bindings);
387
+ if (new_right !== node.right) changed = true;
388
+ const new_body = apply_lazy_transforms(node.body, effective_bindings);
389
+ if (new_body !== node.body) changed = true;
390
+ return changed ? { ...node, right: new_right, body: new_body } : node;
391
+ }
392
+
393
+ // Handle switch-case variable shadowing (const/let inside case consequent arrays)
394
+ if (node.type === 'SwitchStatement') {
395
+ let changed = false;
396
+ const new_discriminant = apply_lazy_transforms(node.discriminant, lazy_bindings);
397
+ if (new_discriminant !== node.discriminant) changed = true;
398
+ const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
399
+ const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
400
+ const effective_bindings =
401
+ case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
402
+ let case_changed = false;
403
+ const new_test = switch_case.test
404
+ ? apply_lazy_transforms(switch_case.test, lazy_bindings)
405
+ : null;
406
+ if (new_test !== switch_case.test) case_changed = true;
407
+ const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
408
+ const transformed = apply_lazy_transforms(stmt, effective_bindings);
409
+ if (transformed !== stmt) case_changed = true;
410
+ return transformed;
411
+ });
412
+ if (case_changed) {
413
+ changed = true;
414
+ return { ...switch_case, test: new_test, consequent: new_consequent };
415
+ }
416
+ return switch_case;
417
+ });
418
+ return changed ? { ...node, discriminant: new_discriminant, cases: new_cases } : node;
419
+ }
420
+
421
+ // Handle assignment: `name = value` → `__lazy0.name = value`
422
+ if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
423
+ const binding = lazy_bindings.get(node.left.name);
424
+ if (binding) {
425
+ return {
426
+ ...node,
427
+ left: binding.read(),
428
+ right: apply_lazy_transforms(node.right, lazy_bindings),
429
+ };
430
+ }
431
+ }
432
+
433
+ // Handle update: `count++` → `__lazy0[0]++`
434
+ if (node.type === 'UpdateExpression' && node.argument.type === 'Identifier') {
435
+ const binding = lazy_bindings.get(node.argument.name);
436
+ if (binding) {
437
+ return { ...node, argument: binding.read() };
438
+ }
439
+ }
440
+
441
+ // Replace lazy variable declaration patterns with generated identifiers
442
+ if (node.type === 'VariableDeclarator' && node.id?.metadata?.lazy_id) {
443
+ const lazy_id = create_generated_identifier(node.id.metadata.lazy_id);
444
+ if (node.id.typeAnnotation) {
445
+ lazy_id.typeAnnotation = node.id.typeAnnotation;
446
+ }
447
+ return {
448
+ ...node,
449
+ id: lazy_id,
450
+ init: apply_lazy_transforms(node.init, lazy_bindings),
451
+ };
452
+ }
453
+
454
+ // Handle identifier references in expression position
455
+ if (node.type === 'Identifier') {
456
+ const binding = lazy_bindings.get(node.name);
457
+ if (binding) {
458
+ return binding.read();
459
+ }
460
+ return node;
461
+ }
462
+
463
+ // Skip JSXIdentifier (component/element names)
464
+ if (node.type === 'JSXIdentifier') return node;
465
+
466
+ // Handle shorthand properties: `{ name }` → `{ name: __lazy0.name }`
467
+ if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
468
+ const binding = lazy_bindings.get(node.value.name);
469
+ if (binding) {
470
+ return {
471
+ ...node,
472
+ shorthand: false,
473
+ value: binding.read(),
474
+ };
475
+ }
476
+ }
477
+
478
+ // Recurse into child nodes
479
+ let changed = false;
480
+ const result = /** @type {any} */ ({});
481
+
482
+ for (const key of Object.keys(node)) {
483
+ if (key === 'loc' || key === 'start' || key === 'end') {
484
+ result[key] = node[key];
485
+ continue;
486
+ }
487
+
488
+ // Skip non-computed property keys (they're labels, not references)
489
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) {
490
+ result[key] = node[key];
491
+ continue;
492
+ }
493
+
494
+ // Skip non-computed member expression property names
495
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) {
496
+ result[key] = node[key];
497
+ continue;
498
+ }
499
+
500
+ // Skip JSXAttribute name
501
+ if (key === 'name' && node.type === 'JSXAttribute') {
502
+ result[key] = node[key];
503
+ continue;
504
+ }
505
+
506
+ // Skip variable declaration id (the lazy declaration itself was already replaced)
507
+ if (key === 'id' && node.type === 'VariableDeclarator') {
508
+ result[key] = node[key];
509
+ continue;
510
+ }
511
+
512
+ const child = node[key];
513
+ const transformed = apply_lazy_transforms(child, lazy_bindings);
514
+ result[key] = transformed;
515
+ if (transformed !== child) changed = true;
516
+ }
517
+
518
+ return changed ? result : node;
519
+ }
520
+
521
+ /**
522
+ * Transform default values in function parameters without touching param names.
523
+ * E.g. `(step = count)` where `count` is a lazy binding → `(step = __lazy0[0])`.
524
+ *
525
+ * @param {any} param
526
+ * @param {Map<string, LazyBinding>} lazy_bindings
527
+ * @returns {any}
528
+ */
529
+ function transform_param_defaults(param, lazy_bindings) {
530
+ if (param?.type === 'AssignmentPattern') {
531
+ const new_right = apply_lazy_transforms(param.right, lazy_bindings);
532
+ if (new_right !== param.right) {
533
+ return { ...param, right: new_right };
534
+ }
535
+ }
536
+ return param;
537
+ }
538
+
539
+ /**
540
+ * Collect names from a pattern that shadow lazy bindings.
541
+ * @param {any} pattern
542
+ * @param {Map<string, LazyBinding>} lazy_bindings
543
+ * @param {Set<string>} shadowed
544
+ */
545
+ function collect_shadowed_names(pattern, lazy_bindings, shadowed) {
546
+ if (!pattern || typeof pattern !== 'object') return;
547
+
548
+ if (pattern.type === 'Identifier' && lazy_bindings.has(pattern.name)) {
549
+ shadowed.add(pattern.name);
550
+ return;
551
+ }
552
+
553
+ if (pattern.type === 'AssignmentPattern') {
554
+ collect_shadowed_names(pattern.left, lazy_bindings, shadowed);
555
+ return;
556
+ }
557
+
558
+ if (pattern.type === 'RestElement') {
559
+ collect_shadowed_names(pattern.argument, lazy_bindings, shadowed);
560
+ return;
561
+ }
562
+
563
+ if (pattern.type === 'ObjectPattern') {
564
+ for (const prop of pattern.properties || []) {
565
+ if (prop.type === 'RestElement') {
566
+ collect_shadowed_names(prop.argument, lazy_bindings, shadowed);
567
+ } else {
568
+ collect_shadowed_names(prop.value, lazy_bindings, shadowed);
569
+ }
570
+ }
571
+ return;
572
+ }
573
+
574
+ if (pattern.type === 'ArrayPattern') {
575
+ for (const element of pattern.elements || []) {
576
+ if (element) collect_shadowed_names(element, lazy_bindings, shadowed);
577
+ }
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Collect variable names declared in block-level statements that shadow lazy bindings.
583
+ * Scans VariableDeclarations (const/let/var) and FunctionDeclarations at the top level of a block.
584
+ *
585
+ * @param {any[]} statements
586
+ * @param {Map<string, LazyBinding>} lazy_bindings
587
+ * @returns {Set<string>}
588
+ */
589
+ function collect_block_shadowed_names(statements, lazy_bindings) {
590
+ /** @type {Set<string>} */
591
+ const shadowed = new Set();
592
+ for (const stmt of statements) {
593
+ if (stmt.type === 'VariableDeclaration') {
594
+ for (const decl of stmt.declarations) {
595
+ // Skip lazy destructuring patterns — they ARE the lazy bindings,
596
+ // not local declarations that shadow them.
597
+ if (decl.id?.metadata?.lazy_id) continue;
598
+ if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
599
+ }
600
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
601
+ if (lazy_bindings.has(stmt.id.name)) {
602
+ shadowed.add(stmt.id.name);
603
+ }
604
+ }
605
+ }
606
+ return shadowed;
607
+ }
608
+
609
+ /**
610
+ * Create a new lazy_bindings map with the shadowed names removed.
611
+ *
612
+ * @param {Map<string, LazyBinding>} lazy_bindings
613
+ * @param {Set<string>} shadowed
614
+ * @returns {Map<string, LazyBinding>}
615
+ */
616
+ function remove_shadowed(lazy_bindings, shadowed) {
617
+ const result = new Map(lazy_bindings);
618
+ for (const name of shadowed) {
619
+ result.delete(name);
620
+ }
621
+ return result;
622
+ }
623
+
624
+ /**
625
+ * Replace lazy parameter patterns with their generated identifiers.
626
+ * A param `&{name, age}: Props` becomes `__lazy0: Props`.
627
+ *
628
+ * @param {any[]} params
629
+ * @returns {any[]}
630
+ */
631
+ function replace_lazy_params(params) {
632
+ return params.map((param) => {
633
+ const pattern = param.type === 'AssignmentPattern' ? param.left : param;
634
+
635
+ if (
636
+ (pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') &&
637
+ pattern.lazy &&
638
+ pattern.metadata?.lazy_id
639
+ ) {
640
+ const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
641
+ if (pattern.typeAnnotation) {
642
+ lazy_id.typeAnnotation = pattern.typeAnnotation;
643
+ }
644
+ if (param.type === 'AssignmentPattern') {
645
+ return { ...param, left: lazy_id };
646
+ }
647
+ return lazy_id;
648
+ }
649
+
650
+ return param;
651
+ });
652
+ }
653
+
110
654
  /**
111
655
  * @param {any} component
112
656
  * @param {TransformContext} transform_context
657
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
113
658
  * @returns {AST.FunctionDeclaration}
114
659
  */
115
- function component_to_function_declaration(component, transform_context) {
116
- const helper_state = create_helper_state(component.id?.name || 'Component');
660
+ function component_to_function_declaration(component, transform_context, walk_helper_state) {
661
+ const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
662
+ const params = component.params || [];
663
+ const body = /** @type {any[]} */ (component.body || []);
664
+
665
+ // Collect param bindings from original patterns (lazy patterns still intact).
666
+ const param_bindings = collect_param_bindings(params);
667
+
668
+ // Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
669
+ // for later replacement. Body bindings (count, setCount, etc.) are still in the
670
+ // original patterns, so collect_statement_bindings during build will find them.
671
+ const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
672
+
673
+ // Save and set context for this component scope
674
+ const saved_helper_state = transform_context.helper_state;
675
+ const saved_bindings = transform_context.available_bindings;
676
+ transform_context.helper_state = helper_state;
677
+ transform_context.available_bindings = new Map(param_bindings);
678
+
679
+ const body_statements = build_component_statements(
680
+ body,
681
+ helper_state,
682
+ param_bindings,
683
+ transform_context,
684
+ );
685
+
686
+ // Replace lazy param patterns with generated identifiers
687
+ const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
688
+
689
+ // Wrap body_statements in a BlockStatement so that apply_lazy_transforms
690
+ // runs collect_block_shadowed_names and detects body-level declarations
691
+ // (e.g. `const name = ...`) that shadow lazy binding names.
692
+ const body_block = /** @type {any} */ ({
693
+ type: 'BlockStatement',
694
+ body: body_statements,
695
+ metadata: { path: [] },
696
+ });
697
+ const final_body =
698
+ lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
699
+
117
700
  const fn = /** @type {any} */ ({
118
701
  type: 'FunctionDeclaration',
119
702
  id: component.id,
120
- params: component.params || [],
121
- body: {
122
- type: 'BlockStatement',
123
- body: build_component_statements(
124
- /** @type {any[]} */ (component.body),
125
- helper_state,
126
- collect_param_bindings(component.params || []),
127
- transform_context,
128
- ),
129
- metadata: { path: [] },
130
- },
703
+ params: final_params,
704
+ body: final_body,
131
705
  async: false,
132
706
  generator: false,
133
707
  metadata: {
134
708
  path: [],
135
709
  is_component: true,
136
- is_method: true,
137
710
  },
138
711
  });
139
712
 
713
+ // Restore context
714
+ transform_context.helper_state = saved_helper_state;
715
+ transform_context.available_bindings = saved_bindings;
716
+
140
717
  fn.metadata.generated_helpers = helper_state.helpers;
718
+ fn.metadata.generated_statics = helper_state.statics;
719
+
720
+ if (fn.id) {
721
+ fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
722
+ ...fn.id.metadata,
723
+ is_component: true,
724
+ });
725
+ }
141
726
 
142
727
  setLocation(fn, /** @type {any} */ (component), true);
143
728
  return fn;
@@ -145,7 +730,7 @@ function component_to_function_declaration(component, transform_context) {
145
730
 
146
731
  /**
147
732
  * @param {any[]} body_nodes
148
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
733
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
149
734
  * @param {Map<string, AST.Identifier>} available_bindings
150
735
  * @param {TransformContext} transform_context
151
736
  * @returns {any[]}
@@ -183,9 +768,12 @@ function build_component_statements(
183
768
  } else {
184
769
  statements.push(child);
185
770
  collect_statement_bindings(child, bindings);
771
+ transform_context.available_bindings = bindings;
186
772
  }
187
773
  }
188
774
 
775
+ hoist_static_render_nodes(render_nodes, transform_context);
776
+
189
777
  const split_node = body_nodes[split_index];
190
778
  const consequent_body =
191
779
  split_node.consequent.type === 'BlockStatement'
@@ -242,9 +830,15 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
242
830
  const statements = [];
243
831
  const render_nodes = [];
244
832
 
833
+ // Create a new bindings map so inner-scope bindings from
834
+ // collect_statement_bindings don't leak to the caller's scope.
835
+ const saved_bindings = transform_context.available_bindings;
836
+ transform_context.available_bindings = new Map(saved_bindings);
837
+
245
838
  for (const child of body_nodes) {
246
839
  if (is_bare_return_statement(child)) {
247
840
  statements.push(create_component_return_statement(render_nodes, child));
841
+ transform_context.available_bindings = saved_bindings;
248
842
  return statements;
249
843
  }
250
844
 
@@ -257,9 +851,12 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
257
851
  render_nodes.push(to_jsx_child(child, transform_context));
258
852
  } else {
259
853
  statements.push(child);
854
+ collect_statement_bindings(child, transform_context.available_bindings);
260
855
  }
261
856
  }
262
857
 
858
+ hoist_static_render_nodes(render_nodes, transform_context);
859
+
263
860
  const return_arg = build_return_expression(render_nodes);
264
861
  if (return_arg || return_null_when_empty) {
265
862
  statements.push({
@@ -268,6 +865,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
268
865
  });
269
866
  }
270
867
 
868
+ transform_context.available_bindings = saved_bindings;
271
869
  return statements;
272
870
  }
273
871
 
@@ -385,7 +983,7 @@ function is_hook_callee(callee) {
385
983
 
386
984
  /**
387
985
  * @param {any[]} body_nodes
388
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
986
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
389
987
  * @param {Map<string, AST.Identifier>} available_bindings
390
988
  * @param {any} source_node
391
989
  * @param {string} suffix
@@ -425,7 +1023,7 @@ function create_helper_component_expression(
425
1023
  /**
426
1024
  * @param {AST.Identifier} helper_id
427
1025
  * @param {any[]} body_nodes
428
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
1026
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
429
1027
  * @param {Map<string, AST.Identifier>} available_bindings
430
1028
  * @param {AST.Identifier[]} helper_bindings
431
1029
  * @param {any} source_node
@@ -460,10 +1058,16 @@ function create_helper_function_declaration(
460
1058
  metadata: {
461
1059
  path: [],
462
1060
  is_component: true,
463
- is_method: true,
464
1061
  },
465
1062
  });
466
1063
 
1064
+ if (fn.id) {
1065
+ fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
1066
+ ...fn.id.metadata,
1067
+ is_component: true,
1068
+ });
1069
+ }
1070
+
467
1071
  return set_loc(fn, source_node);
468
1072
  }
469
1073
 
@@ -538,7 +1142,7 @@ function create_helper_component_element(helper_id, bindings, source_node) {
538
1142
  }
539
1143
 
540
1144
  /**
541
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
1145
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
542
1146
  * @param {string} suffix
543
1147
  * @returns {string}
544
1148
  */
@@ -549,13 +1153,14 @@ function create_helper_name(helper_state, suffix) {
549
1153
 
550
1154
  /**
551
1155
  * @param {string} base_name
552
- * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }}
1156
+ * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
553
1157
  */
554
1158
  function create_helper_state(base_name) {
555
1159
  return {
556
1160
  base_name,
557
1161
  next_id: 0,
558
1162
  helpers: [],
1163
+ statics: [],
559
1164
  };
560
1165
  }
561
1166
 
@@ -635,6 +1240,93 @@ function collect_pattern_bindings(pattern, bindings) {
635
1240
  }
636
1241
  }
637
1242
 
1243
+ /**
1244
+ * Check if a node references any of the given scope bindings.
1245
+ * Used to determine if a JSX element is static and can be hoisted to module level.
1246
+ *
1247
+ * @param {any} node
1248
+ * @param {Map<string, AST.Identifier>} scope_bindings
1249
+ * @returns {boolean}
1250
+ */
1251
+ function references_scope_bindings(node, scope_bindings) {
1252
+ if (!node || typeof node !== 'object') return false;
1253
+ if (scope_bindings.size === 0) return false;
1254
+
1255
+ if (node.type === 'Identifier') {
1256
+ return scope_bindings.has(node.name);
1257
+ }
1258
+
1259
+ // JSXIdentifier is a variable reference when capitalized (tag name like <MyComponent />)
1260
+ // or when it's the object of a JSXMemberExpression (e.g. ui in <ui.Button />)
1261
+ if (node.type === 'JSXIdentifier') {
1262
+ return scope_bindings.has(node.name);
1263
+ }
1264
+
1265
+ if (Array.isArray(node)) {
1266
+ return node.some((child) => references_scope_bindings(child, scope_bindings));
1267
+ }
1268
+
1269
+ for (const key of Object.keys(node)) {
1270
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
1271
+
1272
+ // Skip non-computed, non-shorthand property keys (they are labels, not references)
1273
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
1274
+
1275
+ // Skip non-computed member expression property access
1276
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
1277
+
1278
+ // Skip JSXMemberExpression property (e.g. Button in <Icons.Button /> is a label, not a reference)
1279
+ if (key === 'property' && node.type === 'JSXMemberExpression') continue;
1280
+
1281
+ // Skip JSXAttribute names — they are attribute labels, not variable references
1282
+ if (key === 'name' && node.type === 'JSXAttribute') continue;
1283
+
1284
+ if (references_scope_bindings(node[key], scope_bindings)) return true;
1285
+ }
1286
+
1287
+ return false;
1288
+ }
1289
+
1290
+ /**
1291
+ * Hoist static JSX elements from render_nodes to module level.
1292
+ * A JSX element is static if it doesn't reference any component-scope bindings.
1293
+ * Hoisting prevents React from recreating the element on every render, allowing
1294
+ * the reconciler to skip diffing when it sees the same element identity.
1295
+ *
1296
+ * @param {any[]} render_nodes
1297
+ * @param {TransformContext} transform_context
1298
+ */
1299
+ function hoist_static_render_nodes(render_nodes, transform_context) {
1300
+ if (!transform_context.helper_state) return;
1301
+
1302
+ for (let i = 0; i < render_nodes.length; i++) {
1303
+ const node = render_nodes[i];
1304
+ if (node.type !== 'JSXElement') continue;
1305
+ if (references_scope_bindings(node, transform_context.available_bindings)) continue;
1306
+
1307
+ const name = create_helper_name(transform_context.helper_state, 'static');
1308
+ const id = create_generated_identifier(name);
1309
+
1310
+ transform_context.helper_state.statics.push(
1311
+ /** @type {any} */ ({
1312
+ type: 'VariableDeclaration',
1313
+ kind: 'const',
1314
+ declarations: [
1315
+ {
1316
+ type: 'VariableDeclarator',
1317
+ id,
1318
+ init: node,
1319
+ metadata: { path: [] },
1320
+ },
1321
+ ],
1322
+ metadata: { path: [] },
1323
+ }),
1324
+ );
1325
+
1326
+ render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
1327
+ }
1328
+ }
1329
+
638
1330
  /**
639
1331
  * @param {AST.Identifier} identifier
640
1332
  * @returns {AST.Identifier}
@@ -669,9 +1361,11 @@ function create_null_literal() {
669
1361
  function expand_component_helpers(program) {
670
1362
  program.body = program.body.flatMap((statement) => {
671
1363
  if (statement.type === 'FunctionDeclaration') {
672
- const helpers = /** @type {any} */ (statement.metadata)?.generated_helpers;
673
- if (helpers?.length) {
674
- return [...helpers, statement];
1364
+ const meta = /** @type {any} */ (statement.metadata);
1365
+ const statics = meta?.generated_statics || [];
1366
+ const helpers = meta?.generated_helpers || [];
1367
+ if (statics.length || helpers.length) {
1368
+ return [...statics, ...helpers, statement];
675
1369
  }
676
1370
  }
677
1371
 
@@ -680,9 +1374,11 @@ function expand_component_helpers(program) {
680
1374
  statement.type === 'ExportDefaultDeclaration') &&
681
1375
  statement.declaration?.type === 'FunctionDeclaration'
682
1376
  ) {
683
- const helpers = /** @type {any} */ (statement.declaration.metadata)?.generated_helpers;
684
- if (helpers?.length) {
685
- return [...helpers, statement];
1377
+ const meta = /** @type {any} */ (statement.declaration.metadata);
1378
+ const statics = meta?.generated_statics || [];
1379
+ const helpers = meta?.generated_helpers || [];
1380
+ if (statics.length || helpers.length) {
1381
+ return [...statics, ...helpers, statement];
686
1382
  }
687
1383
  }
688
1384
 
@@ -799,6 +1495,22 @@ function is_style_element(node) {
799
1495
  );
800
1496
  }
801
1497
 
1498
+ /**
1499
+ * @param {any} node
1500
+ * @returns {boolean}
1501
+ */
1502
+ function is_composite_element(node) {
1503
+ if (!node || node.type !== 'Element' || !node.id) {
1504
+ return false;
1505
+ }
1506
+
1507
+ if (node.id.type === 'Identifier') {
1508
+ return /^[A-Z]/.test(node.id.name);
1509
+ }
1510
+
1511
+ return node.id.type === 'MemberExpression';
1512
+ }
1513
+
802
1514
  /**
803
1515
  * Recursively walk Element nodes within a component body and add the hash
804
1516
  * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
@@ -819,7 +1531,7 @@ function annotate_with_hash(node, hash) {
819
1531
  }
820
1532
 
821
1533
  if (node.type === 'Element') {
822
- if (!is_style_element(node)) {
1534
+ if (!is_style_element(node) && !is_composite_element(node)) {
823
1535
  add_hash_class(node, hash);
824
1536
  }
825
1537
  if (Array.isArray(node.children)) {
@@ -1114,11 +1826,17 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1114
1826
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1115
1827
  source_node,
1116
1828
  );
1829
+ const helper_bindings = Array.from(transform_context.available_bindings.values());
1830
+
1831
+ // Save and isolate bindings for the helper body
1832
+ const saved_bindings = transform_context.available_bindings;
1833
+ transform_context.available_bindings = new Map(saved_bindings);
1834
+
1117
1835
  const helper_fn = set_loc(
1118
1836
  /** @type {any} */ ({
1119
1837
  type: 'FunctionDeclaration',
1120
1838
  id: helper_id,
1121
- params: [],
1839
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1122
1840
  body: {
1123
1841
  type: 'BlockStatement',
1124
1842
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1135,6 +1853,19 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1135
1853
  source_node,
1136
1854
  );
1137
1855
 
1856
+ // Restore bindings
1857
+ transform_context.available_bindings = saved_bindings;
1858
+
1859
+ // Register helper for hoisting to module level
1860
+ if (transform_context.helper_state) {
1861
+ transform_context.helper_state.helpers.push(helper_fn);
1862
+
1863
+ return to_jsx_expression_container(
1864
+ /** @type {any} */ (create_helper_component_element(helper_id, helper_bindings, source_node)),
1865
+ source_node,
1866
+ );
1867
+ }
1868
+
1138
1869
  return to_jsx_expression_container(
1139
1870
  /** @type {any} */ ({
1140
1871
  type: 'CallExpression',
@@ -1147,7 +1878,7 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1147
1878
  helper_fn,
1148
1879
  {
1149
1880
  type: 'ReturnStatement',
1150
- argument: create_helper_component_element(helper_id, [], source_node),
1881
+ argument: create_helper_component_element(helper_id, helper_bindings, source_node),
1151
1882
  metadata: { path: [] },
1152
1883
  },
1153
1884
  ],
@@ -1176,8 +1907,10 @@ function create_local_statement_component_name(transform_context) {
1176
1907
  }
1177
1908
 
1178
1909
  /**
1179
- * Wraps a list of body nodes into a locally-declared component and returns
1180
- * statements that declare the component then return `<ComponentName />`.
1910
+ * Wraps a list of body nodes into a component and returns
1911
+ * statements that return `<ComponentName prop1={prop1} ... />`.
1912
+ * The component is hoisted to module level via helper_state to avoid
1913
+ * recreating the component identity on every render.
1181
1914
  * Used when a control flow branch contains hook calls that must be moved
1182
1915
  * into their own component boundary to satisfy the Rules of Hooks.
1183
1916
  *
@@ -1192,12 +1925,17 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1192
1925
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1193
1926
  source_node,
1194
1927
  );
1928
+ const helper_bindings = Array.from(transform_context.available_bindings.values());
1929
+
1930
+ // Save and isolate bindings for the helper body
1931
+ const saved_bindings = transform_context.available_bindings;
1932
+ transform_context.available_bindings = new Map(saved_bindings);
1195
1933
 
1196
1934
  const helper_fn = set_loc(
1197
1935
  /** @type {any} */ ({
1198
1936
  type: 'FunctionDeclaration',
1199
1937
  id: helper_id,
1200
- params: [],
1938
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1201
1939
  body: {
1202
1940
  type: 'BlockStatement',
1203
1941
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1214,7 +1952,19 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1214
1952
  source_node,
1215
1953
  );
1216
1954
 
1217
- const component_element = create_helper_component_element(helper_id, [], source_node);
1955
+ // Restore bindings
1956
+ transform_context.available_bindings = saved_bindings;
1957
+
1958
+ // Register helper for hoisting to module level
1959
+ if (transform_context.helper_state) {
1960
+ transform_context.helper_state.helpers.push(helper_fn);
1961
+ }
1962
+
1963
+ const component_element = create_helper_component_element(
1964
+ helper_id,
1965
+ helper_bindings,
1966
+ source_node,
1967
+ );
1218
1968
 
1219
1969
  if (key_expression) {
1220
1970
  component_element.openingElement.attributes.push(
@@ -1227,8 +1977,20 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1227
1977
  );
1228
1978
  }
1229
1979
 
1980
+ // When helper_state is null (no enclosing component context), inline the
1981
+ // helper via an IIFE so the function declaration isn't silently dropped.
1982
+ if (!transform_context.helper_state) {
1983
+ return [
1984
+ helper_fn,
1985
+ {
1986
+ type: 'ReturnStatement',
1987
+ argument: component_element,
1988
+ metadata: { path: [] },
1989
+ },
1990
+ ];
1991
+ }
1992
+
1230
1993
  return [
1231
- helper_fn,
1232
1994
  {
1233
1995
  type: 'ReturnStatement',
1234
1996
  argument: component_element,
@@ -1274,6 +2036,7 @@ function to_jsx_child(node, transform_context) {
1274
2036
  case 'Element':
1275
2037
  return to_jsx_element(node, transform_context);
1276
2038
  case 'Text':
2039
+ return to_jsx_expression_container(to_text_expression(node.expression, node), node);
1277
2040
  case 'TSRXExpression':
1278
2041
  return to_jsx_expression_container(node.expression, node);
1279
2042
  case 'IfStatement':
@@ -1380,6 +2143,20 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1380
2143
  const has_hooks = body_contains_top_level_hook_call(loop_body);
1381
2144
  const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1382
2145
 
2146
+ // Add loop params to available bindings so hoisted helpers receive them as props
2147
+ const saved_bindings = transform_context.available_bindings;
2148
+ transform_context.available_bindings = new Map(saved_bindings);
2149
+ for (const param of loop_params) {
2150
+ collect_pattern_bindings(param, transform_context.available_bindings);
2151
+ }
2152
+
2153
+ const body_statements = has_hooks
2154
+ ? hook_safe_render_statements(loop_body, key_expression, transform_context)
2155
+ : build_render_statements(loop_body, true, transform_context);
2156
+
2157
+ // Restore bindings
2158
+ transform_context.available_bindings = saved_bindings;
2159
+
1383
2160
  return to_jsx_expression_container(
1384
2161
  /** @type {any} */ ({
1385
2162
  type: 'CallExpression',
@@ -1397,9 +2174,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1397
2174
  params: loop_params,
1398
2175
  body: /** @type {any} */ ({
1399
2176
  type: 'BlockStatement',
1400
- body: has_hooks
1401
- ? hook_safe_render_statements(loop_body, key_expression, transform_context)
1402
- : build_render_statements(loop_body, true, transform_context),
2177
+ body: body_statements,
1403
2178
  metadata: { path: [] },
1404
2179
  }),
1405
2180
  async: false,
@@ -1541,6 +2316,15 @@ function try_statement_to_jsx_child(node, transform_context) {
1541
2316
  }
1542
2317
 
1543
2318
  const catch_body_nodes = handler.body.body || [];
2319
+
2320
+ // Add catch params to available_bindings so static hoisting
2321
+ // correctly identifies references to err/reset as non-static
2322
+ const saved_catch_bindings = transform_context.available_bindings;
2323
+ transform_context.available_bindings = new Map(saved_catch_bindings);
2324
+ for (const param of catch_params) {
2325
+ collect_pattern_bindings(param, transform_context.available_bindings);
2326
+ }
2327
+
1544
2328
  const fallback_fn = {
1545
2329
  type: 'ArrowFunctionExpression',
1546
2330
  params: catch_params,
@@ -1555,6 +2339,8 @@ function try_statement_to_jsx_child(node, transform_context) {
1555
2339
  metadata: { path: [] },
1556
2340
  };
1557
2341
 
2342
+ transform_context.available_bindings = saved_catch_bindings;
2343
+
1558
2344
  result = create_jsx_element(
1559
2345
  'TsrxErrorBoundary',
1560
2346
  [
@@ -1826,6 +2612,54 @@ function to_jsx_expression_container(expression, source_node = expression) {
1826
2612
  });
1827
2613
  }
1828
2614
 
2615
+ /**
2616
+ * Ripple's `{text expr}` always renders text, even for booleans and objects.
2617
+ * React's normal `{expr}` child semantics would drop booleans and render
2618
+ * elements as elements, so we coerce to a text value explicitly.
2619
+ * @param {AST.Expression} expression
2620
+ * @param {any} [source_node]
2621
+ * @returns {AST.Expression}
2622
+ */
2623
+ function to_text_expression(expression, source_node = expression) {
2624
+ return set_loc(
2625
+ /** @type {AST.Expression} */ ({
2626
+ type: 'ConditionalExpression',
2627
+ test: {
2628
+ type: 'BinaryExpression',
2629
+ operator: '==',
2630
+ left: clone_expression_node(expression),
2631
+ right: {
2632
+ type: 'Literal',
2633
+ value: null,
2634
+ raw: 'null',
2635
+ metadata: { path: [] },
2636
+ },
2637
+ metadata: { path: [] },
2638
+ },
2639
+ consequent: {
2640
+ type: 'Literal',
2641
+ value: '',
2642
+ raw: "''",
2643
+ metadata: { path: [] },
2644
+ },
2645
+ alternate: {
2646
+ type: 'BinaryExpression',
2647
+ operator: '+',
2648
+ left: clone_expression_node(expression),
2649
+ right: {
2650
+ type: 'Literal',
2651
+ value: '',
2652
+ raw: "''",
2653
+ metadata: { path: [] },
2654
+ },
2655
+ metadata: { path: [] },
2656
+ },
2657
+ metadata: { path: [] },
2658
+ }),
2659
+ source_node,
2660
+ );
2661
+ }
2662
+
1829
2663
  /**
1830
2664
  * @param {any} attr
1831
2665
  * @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
package/types/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Program } from 'estree';
2
- import type { VolarMappingsResult } from '@tsrx/core/types';
2
+ import type { ParseOptions, VolarMappingsResult } from '@tsrx/core/types';
3
3
 
4
- export function parse(source: string, filename?: string): Program;
4
+ export function parse(source: string, filename?: string, options?: ParseOptions): Program;
5
5
 
6
6
  export function compile(
7
7
  source: string,
@@ -12,4 +12,8 @@ export function compile(
12
12
  css: { code: string; hash: string } | null;
13
13
  };
14
14
 
15
- export function compile_to_volar_mappings(source: string, filename?: string): VolarMappingsResult;
15
+ export function compile_to_volar_mappings(
16
+ source: string,
17
+ filename?: string,
18
+ options?: ParseOptions,
19
+ ): VolarMappingsResult;