@tsrx/react 0.0.2 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/transform.js +790 -35
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.2",
6
+ "version": "0.0.3",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
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 }) {
@@ -109,27 +150,558 @@ export function transform(ast, source, filename) {
109
150
  return { ast: expanded, code: result.code, map: result.map, css };
110
151
  }
111
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
+
112
654
  /**
113
655
  * @param {any} component
114
656
  * @param {TransformContext} transform_context
657
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
115
658
  * @returns {AST.FunctionDeclaration}
116
659
  */
117
- function component_to_function_declaration(component, transform_context) {
118
- 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
+
119
700
  const fn = /** @type {any} */ ({
120
701
  type: 'FunctionDeclaration',
121
702
  id: component.id,
122
- params: component.params || [],
123
- body: {
124
- type: 'BlockStatement',
125
- body: build_component_statements(
126
- /** @type {any[]} */ (component.body),
127
- helper_state,
128
- collect_param_bindings(component.params || []),
129
- transform_context,
130
- ),
131
- metadata: { path: [] },
132
- },
703
+ params: final_params,
704
+ body: final_body,
133
705
  async: false,
134
706
  generator: false,
135
707
  metadata: {
@@ -138,7 +710,12 @@ function component_to_function_declaration(component, transform_context) {
138
710
  },
139
711
  });
140
712
 
713
+ // Restore context
714
+ transform_context.helper_state = saved_helper_state;
715
+ transform_context.available_bindings = saved_bindings;
716
+
141
717
  fn.metadata.generated_helpers = helper_state.helpers;
718
+ fn.metadata.generated_statics = helper_state.statics;
142
719
 
143
720
  if (fn.id) {
144
721
  fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
@@ -153,7 +730,7 @@ function component_to_function_declaration(component, transform_context) {
153
730
 
154
731
  /**
155
732
  * @param {any[]} body_nodes
156
- * @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
157
734
  * @param {Map<string, AST.Identifier>} available_bindings
158
735
  * @param {TransformContext} transform_context
159
736
  * @returns {any[]}
@@ -191,9 +768,12 @@ function build_component_statements(
191
768
  } else {
192
769
  statements.push(child);
193
770
  collect_statement_bindings(child, bindings);
771
+ transform_context.available_bindings = bindings;
194
772
  }
195
773
  }
196
774
 
775
+ hoist_static_render_nodes(render_nodes, transform_context);
776
+
197
777
  const split_node = body_nodes[split_index];
198
778
  const consequent_body =
199
779
  split_node.consequent.type === 'BlockStatement'
@@ -250,9 +830,15 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
250
830
  const statements = [];
251
831
  const render_nodes = [];
252
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
+
253
838
  for (const child of body_nodes) {
254
839
  if (is_bare_return_statement(child)) {
255
840
  statements.push(create_component_return_statement(render_nodes, child));
841
+ transform_context.available_bindings = saved_bindings;
256
842
  return statements;
257
843
  }
258
844
 
@@ -265,9 +851,12 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
265
851
  render_nodes.push(to_jsx_child(child, transform_context));
266
852
  } else {
267
853
  statements.push(child);
854
+ collect_statement_bindings(child, transform_context.available_bindings);
268
855
  }
269
856
  }
270
857
 
858
+ hoist_static_render_nodes(render_nodes, transform_context);
859
+
271
860
  const return_arg = build_return_expression(render_nodes);
272
861
  if (return_arg || return_null_when_empty) {
273
862
  statements.push({
@@ -276,6 +865,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
276
865
  });
277
866
  }
278
867
 
868
+ transform_context.available_bindings = saved_bindings;
279
869
  return statements;
280
870
  }
281
871
 
@@ -393,7 +983,7 @@ function is_hook_callee(callee) {
393
983
 
394
984
  /**
395
985
  * @param {any[]} body_nodes
396
- * @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
397
987
  * @param {Map<string, AST.Identifier>} available_bindings
398
988
  * @param {any} source_node
399
989
  * @param {string} suffix
@@ -433,7 +1023,7 @@ function create_helper_component_expression(
433
1023
  /**
434
1024
  * @param {AST.Identifier} helper_id
435
1025
  * @param {any[]} body_nodes
436
- * @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
437
1027
  * @param {Map<string, AST.Identifier>} available_bindings
438
1028
  * @param {AST.Identifier[]} helper_bindings
439
1029
  * @param {any} source_node
@@ -552,7 +1142,7 @@ function create_helper_component_element(helper_id, bindings, source_node) {
552
1142
  }
553
1143
 
554
1144
  /**
555
- * @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
556
1146
  * @param {string} suffix
557
1147
  * @returns {string}
558
1148
  */
@@ -563,13 +1153,14 @@ function create_helper_name(helper_state, suffix) {
563
1153
 
564
1154
  /**
565
1155
  * @param {string} base_name
566
- * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }}
1156
+ * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
567
1157
  */
568
1158
  function create_helper_state(base_name) {
569
1159
  return {
570
1160
  base_name,
571
1161
  next_id: 0,
572
1162
  helpers: [],
1163
+ statics: [],
573
1164
  };
574
1165
  }
575
1166
 
@@ -649,6 +1240,93 @@ function collect_pattern_bindings(pattern, bindings) {
649
1240
  }
650
1241
  }
651
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
+
652
1330
  /**
653
1331
  * @param {AST.Identifier} identifier
654
1332
  * @returns {AST.Identifier}
@@ -683,9 +1361,11 @@ function create_null_literal() {
683
1361
  function expand_component_helpers(program) {
684
1362
  program.body = program.body.flatMap((statement) => {
685
1363
  if (statement.type === 'FunctionDeclaration') {
686
- const helpers = /** @type {any} */ (statement.metadata)?.generated_helpers;
687
- if (helpers?.length) {
688
- 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];
689
1369
  }
690
1370
  }
691
1371
 
@@ -694,9 +1374,11 @@ function expand_component_helpers(program) {
694
1374
  statement.type === 'ExportDefaultDeclaration') &&
695
1375
  statement.declaration?.type === 'FunctionDeclaration'
696
1376
  ) {
697
- const helpers = /** @type {any} */ (statement.declaration.metadata)?.generated_helpers;
698
- if (helpers?.length) {
699
- 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];
700
1382
  }
701
1383
  }
702
1384
 
@@ -1144,11 +1826,17 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1144
1826
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1145
1827
  source_node,
1146
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
+
1147
1835
  const helper_fn = set_loc(
1148
1836
  /** @type {any} */ ({
1149
1837
  type: 'FunctionDeclaration',
1150
1838
  id: helper_id,
1151
- params: [],
1839
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1152
1840
  body: {
1153
1841
  type: 'BlockStatement',
1154
1842
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1165,6 +1853,19 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1165
1853
  source_node,
1166
1854
  );
1167
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
+
1168
1869
  return to_jsx_expression_container(
1169
1870
  /** @type {any} */ ({
1170
1871
  type: 'CallExpression',
@@ -1177,7 +1878,7 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1177
1878
  helper_fn,
1178
1879
  {
1179
1880
  type: 'ReturnStatement',
1180
- argument: create_helper_component_element(helper_id, [], source_node),
1881
+ argument: create_helper_component_element(helper_id, helper_bindings, source_node),
1181
1882
  metadata: { path: [] },
1182
1883
  },
1183
1884
  ],
@@ -1206,8 +1907,10 @@ function create_local_statement_component_name(transform_context) {
1206
1907
  }
1207
1908
 
1208
1909
  /**
1209
- * Wraps a list of body nodes into a locally-declared component and returns
1210
- * 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.
1211
1914
  * Used when a control flow branch contains hook calls that must be moved
1212
1915
  * into their own component boundary to satisfy the Rules of Hooks.
1213
1916
  *
@@ -1222,12 +1925,17 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1222
1925
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1223
1926
  source_node,
1224
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);
1225
1933
 
1226
1934
  const helper_fn = set_loc(
1227
1935
  /** @type {any} */ ({
1228
1936
  type: 'FunctionDeclaration',
1229
1937
  id: helper_id,
1230
- params: [],
1938
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1231
1939
  body: {
1232
1940
  type: 'BlockStatement',
1233
1941
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1244,7 +1952,19 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1244
1952
  source_node,
1245
1953
  );
1246
1954
 
1247
- 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
+ );
1248
1968
 
1249
1969
  if (key_expression) {
1250
1970
  component_element.openingElement.attributes.push(
@@ -1257,8 +1977,20 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1257
1977
  );
1258
1978
  }
1259
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
+
1260
1993
  return [
1261
- helper_fn,
1262
1994
  {
1263
1995
  type: 'ReturnStatement',
1264
1996
  argument: component_element,
@@ -1411,6 +2143,20 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1411
2143
  const has_hooks = body_contains_top_level_hook_call(loop_body);
1412
2144
  const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1413
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
+
1414
2160
  return to_jsx_expression_container(
1415
2161
  /** @type {any} */ ({
1416
2162
  type: 'CallExpression',
@@ -1428,9 +2174,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1428
2174
  params: loop_params,
1429
2175
  body: /** @type {any} */ ({
1430
2176
  type: 'BlockStatement',
1431
- body: has_hooks
1432
- ? hook_safe_render_statements(loop_body, key_expression, transform_context)
1433
- : build_render_statements(loop_body, true, transform_context),
2177
+ body: body_statements,
1434
2178
  metadata: { path: [] },
1435
2179
  }),
1436
2180
  async: false,
@@ -1572,6 +2316,15 @@ function try_statement_to_jsx_child(node, transform_context) {
1572
2316
  }
1573
2317
 
1574
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
+
1575
2328
  const fallback_fn = {
1576
2329
  type: 'ArrowFunctionExpression',
1577
2330
  params: catch_params,
@@ -1586,6 +2339,8 @@ function try_statement_to_jsx_child(node, transform_context) {
1586
2339
  metadata: { path: [] },
1587
2340
  };
1588
2341
 
2342
+ transform_context.available_bindings = saved_catch_bindings;
2343
+
1589
2344
  result = create_jsx_element(
1590
2345
  'TsrxErrorBoundary',
1591
2346
  [