@tsrx/react 0.0.2 → 0.0.4

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.2",
6
+ "version": "0.0.4",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** @import * as AST from 'estree' */
2
- /** @import { CodeMapping, ParseOptions } from '@tsrx/core/types' */
2
+ /** @import { ParseOptions } from '@tsrx/core/types' */
3
3
 
4
- import { createVolarMappingsResult, parseModule } from '@tsrx/core';
4
+ import { createVolarMappingsResult, dedupeMappings, parseModule } from '@tsrx/core';
5
5
  import { transform } from './transform.js';
6
6
 
7
7
  /**
@@ -51,56 +51,6 @@ export function compile_to_volar_mappings(source, filename, options) {
51
51
 
52
52
  return {
53
53
  ...result,
54
- mappings: dedupe_mappings(result.mappings),
54
+ mappings: dedupeMappings(result.mappings),
55
55
  };
56
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;
106
- }
package/src/transform.js CHANGED
@@ -4,16 +4,33 @@
4
4
  import { walk } from 'zimmerframe';
5
5
  import { print } from 'esrap';
6
6
  import tsx from 'esrap/languages/tsx';
7
- import { renderStylesheets, setLocation } from '@tsrx/core';
7
+ import {
8
+ renderStylesheets,
9
+ setLocation,
10
+ applyLazyTransforms as apply_lazy_transforms,
11
+ collectLazyBindingsFromComponent as collect_lazy_bindings_from_component,
12
+ preallocateLazyIds as preallocate_lazy_ids,
13
+ replaceLazyParams as replace_lazy_params,
14
+ prepareStylesheetForRender as prepare_stylesheet_for_render,
15
+ annotateComponentWithHash as annotate_component_with_hash,
16
+ } from '@tsrx/core';
8
17
 
9
18
  /**
10
19
  * @typedef {{
11
20
  * local_statement_component_index: number,
12
21
  * needs_error_boundary: boolean,
13
22
  * needs_suspense: boolean,
23
+ * helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
24
+ * available_bindings: Map<string, AST.Identifier>,
25
+ * lazy_next_id: number,
26
+ * current_css_hash: string | null,
14
27
  * }} TransformContext
15
28
  */
16
29
 
30
+ /**
31
+ * @typedef {{ source_name: string, read: () => any }} LazyBinding
32
+ */
33
+
17
34
  /**
18
35
  * Transform a parsed tsrx-react AST into a TSX/JSX module.
19
36
  *
@@ -39,8 +56,14 @@ export function transform(ast, source, filename) {
39
56
  local_statement_component_index: 0,
40
57
  needs_error_boundary: false,
41
58
  needs_suspense: false,
59
+ helper_state: null,
60
+ available_bindings: new Map(),
61
+ lazy_next_id: 0,
62
+ current_css_hash: null,
42
63
  };
43
64
 
65
+ preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
66
+
44
67
  walk(/** @type {any} */ (ast), transform_context, {
45
68
  Component(node, { next, state }) {
46
69
  const as_any = /** @type {any} */ (node);
@@ -56,8 +79,42 @@ export function transform(ast, source, filename) {
56
79
 
57
80
  const transformed = walk(/** @type {any} */ (ast), transform_context, {
58
81
  Component(node, { next, state }) {
82
+ const as_any = /** @type {any} */ (node);
83
+
84
+ // Set up helper_state and bindings BEFORE next() so that nested
85
+ // hook_safe_* calls (inside Element children) can register helpers
86
+ // and access available bindings during the bottom-up walk.
87
+ const helper_state = create_helper_state(as_any.id?.name || 'Component');
88
+ const saved_helper_state = state.helper_state;
89
+ const saved_bindings = state.available_bindings;
90
+ const saved_css_hash = state.current_css_hash;
91
+ state.helper_state = helper_state;
92
+ state.current_css_hash = as_any.css ? as_any.css.hash : null;
93
+
94
+ // Pre-collect component body bindings (params + top-level statements)
95
+ // so that Element children processed during the bottom-up walk can see
96
+ // the full scope. Without this, hoisted helpers would miss body-level
97
+ // variables like `const [x] = useState(...)` and produce ReferenceErrors.
98
+ // Only collect up to the split point — bindings declared after a
99
+ // hook-safe split aren't in scope at the return statement and would
100
+ // cause ReferenceErrors if passed as helper props.
101
+ const body_bindings = collect_param_bindings(as_any.params || []);
102
+ const body = as_any.body || [];
103
+ const split_index = find_hook_safe_split_index(body);
104
+ const collect_end = split_index === -1 ? body.length : split_index;
105
+ for (let i = 0; i < collect_end; i += 1) {
106
+ collect_statement_bindings(body[i], body_bindings);
107
+ }
108
+ state.available_bindings = body_bindings;
109
+
59
110
  const inner = /** @type {any} */ (next() ?? node);
60
- return /** @type {any} */ (component_to_function_declaration(inner, state));
111
+
112
+ // Restore context
113
+ state.helper_state = saved_helper_state;
114
+ state.available_bindings = saved_bindings;
115
+ state.current_css_hash = saved_css_hash;
116
+
117
+ return /** @type {any} */ (component_to_function_declaration(inner, state, helper_state));
61
118
  },
62
119
 
63
120
  Tsx(node, { next }) {
@@ -86,12 +143,30 @@ export function transform(ast, source, filename) {
86
143
  const inner = /** @type {any} */ (next() ?? node);
87
144
  return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
88
145
  },
146
+
147
+ MemberExpression(node, { next, state }) {
148
+ const as_any = /** @type {any} */ (node);
149
+ if (as_any.object && as_any.object.type === 'StyleIdentifier' && state.current_css_hash) {
150
+ const class_name = as_any.computed ? as_any.property.value : as_any.property.name;
151
+ const value = `${state.current_css_hash} ${class_name}`;
152
+ return /** @type {any} */ ({ type: 'Literal', value, raw: JSON.stringify(value) });
153
+ }
154
+ return next();
155
+ },
89
156
  });
90
157
 
91
158
  const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
92
159
  inject_try_imports(expanded, transform_context);
93
160
 
94
- const result = print(/** @type {any} */ (expanded), tsx(), {
161
+ // Apply lazy destructuring transforms to module-level code (top-level function
162
+ // declarations, arrow functions, etc.). Component bodies have already been
163
+ // transformed inside component_to_function_declaration; this catches plain
164
+ // functions outside components and any lazy patterns in module scope.
165
+ const final_program = /** @type {any} */ (
166
+ apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
167
+ );
168
+
169
+ const result = print(/** @type {any} */ (final_program), tsx(), {
95
170
  sourceMapSource: filename,
96
171
  sourceMapContent: source,
97
172
  });
@@ -106,30 +181,60 @@ export function transform(ast, source, filename) {
106
181
  }
107
182
  : null;
108
183
 
109
- return { ast: expanded, code: result.code, map: result.map, css };
184
+ return { ast: final_program, code: result.code, map: result.map, css };
110
185
  }
111
186
 
112
187
  /**
113
188
  * @param {any} component
114
189
  * @param {TransformContext} transform_context
190
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
115
191
  * @returns {AST.FunctionDeclaration}
116
192
  */
117
- function component_to_function_declaration(component, transform_context) {
118
- const helper_state = create_helper_state(component.id?.name || 'Component');
193
+ function component_to_function_declaration(component, transform_context, walk_helper_state) {
194
+ const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
195
+ const params = component.params || [];
196
+ const body = /** @type {any[]} */ (component.body || []);
197
+
198
+ // Collect param bindings from original patterns (lazy patterns still intact).
199
+ const param_bindings = collect_param_bindings(params);
200
+
201
+ // Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
202
+ // for later replacement. Body bindings (count, setCount, etc.) are still in the
203
+ // original patterns, so collect_statement_bindings during build will find them.
204
+ const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
205
+
206
+ // Save and set context for this component scope
207
+ const saved_helper_state = transform_context.helper_state;
208
+ const saved_bindings = transform_context.available_bindings;
209
+ transform_context.helper_state = helper_state;
210
+ transform_context.available_bindings = new Map(param_bindings);
211
+
212
+ const body_statements = build_component_statements(
213
+ body,
214
+ helper_state,
215
+ param_bindings,
216
+ transform_context,
217
+ );
218
+
219
+ // Replace lazy param patterns with generated identifiers
220
+ const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
221
+
222
+ // Wrap body_statements in a BlockStatement so that apply_lazy_transforms
223
+ // runs collect_block_shadowed_names and detects body-level declarations
224
+ // (e.g. `const name = ...`) that shadow lazy binding names.
225
+ const body_block = /** @type {any} */ ({
226
+ type: 'BlockStatement',
227
+ body: body_statements,
228
+ metadata: { path: [] },
229
+ });
230
+ const final_body =
231
+ lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
232
+
119
233
  const fn = /** @type {any} */ ({
120
234
  type: 'FunctionDeclaration',
121
235
  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
- },
236
+ params: final_params,
237
+ body: final_body,
133
238
  async: false,
134
239
  generator: false,
135
240
  metadata: {
@@ -138,7 +243,12 @@ function component_to_function_declaration(component, transform_context) {
138
243
  },
139
244
  });
140
245
 
246
+ // Restore context
247
+ transform_context.helper_state = saved_helper_state;
248
+ transform_context.available_bindings = saved_bindings;
249
+
141
250
  fn.metadata.generated_helpers = helper_state.helpers;
251
+ fn.metadata.generated_statics = helper_state.statics;
142
252
 
143
253
  if (fn.id) {
144
254
  fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
@@ -153,7 +263,7 @@ function component_to_function_declaration(component, transform_context) {
153
263
 
154
264
  /**
155
265
  * @param {any[]} body_nodes
156
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
266
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
157
267
  * @param {Map<string, AST.Identifier>} available_bindings
158
268
  * @param {TransformContext} transform_context
159
269
  * @returns {any[]}
@@ -191,9 +301,12 @@ function build_component_statements(
191
301
  } else {
192
302
  statements.push(child);
193
303
  collect_statement_bindings(child, bindings);
304
+ transform_context.available_bindings = bindings;
194
305
  }
195
306
  }
196
307
 
308
+ hoist_static_render_nodes(render_nodes, transform_context);
309
+
197
310
  const split_node = body_nodes[split_index];
198
311
  const consequent_body =
199
312
  split_node.consequent.type === 'BlockStatement'
@@ -250,9 +363,15 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
250
363
  const statements = [];
251
364
  const render_nodes = [];
252
365
 
366
+ // Create a new bindings map so inner-scope bindings from
367
+ // collect_statement_bindings don't leak to the caller's scope.
368
+ const saved_bindings = transform_context.available_bindings;
369
+ transform_context.available_bindings = new Map(saved_bindings);
370
+
253
371
  for (const child of body_nodes) {
254
372
  if (is_bare_return_statement(child)) {
255
373
  statements.push(create_component_return_statement(render_nodes, child));
374
+ transform_context.available_bindings = saved_bindings;
256
375
  return statements;
257
376
  }
258
377
 
@@ -265,9 +384,12 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
265
384
  render_nodes.push(to_jsx_child(child, transform_context));
266
385
  } else {
267
386
  statements.push(child);
387
+ collect_statement_bindings(child, transform_context.available_bindings);
268
388
  }
269
389
  }
270
390
 
391
+ hoist_static_render_nodes(render_nodes, transform_context);
392
+
271
393
  const return_arg = build_return_expression(render_nodes);
272
394
  if (return_arg || return_null_when_empty) {
273
395
  statements.push({
@@ -276,6 +398,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
276
398
  });
277
399
  }
278
400
 
401
+ transform_context.available_bindings = saved_bindings;
279
402
  return statements;
280
403
  }
281
404
 
@@ -393,7 +516,7 @@ function is_hook_callee(callee) {
393
516
 
394
517
  /**
395
518
  * @param {any[]} body_nodes
396
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
519
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
397
520
  * @param {Map<string, AST.Identifier>} available_bindings
398
521
  * @param {any} source_node
399
522
  * @param {string} suffix
@@ -433,7 +556,7 @@ function create_helper_component_expression(
433
556
  /**
434
557
  * @param {AST.Identifier} helper_id
435
558
  * @param {any[]} body_nodes
436
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
559
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
437
560
  * @param {Map<string, AST.Identifier>} available_bindings
438
561
  * @param {AST.Identifier[]} helper_bindings
439
562
  * @param {any} source_node
@@ -552,7 +675,7 @@ function create_helper_component_element(helper_id, bindings, source_node) {
552
675
  }
553
676
 
554
677
  /**
555
- * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
678
+ * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
556
679
  * @param {string} suffix
557
680
  * @returns {string}
558
681
  */
@@ -563,13 +686,14 @@ function create_helper_name(helper_state, suffix) {
563
686
 
564
687
  /**
565
688
  * @param {string} base_name
566
- * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }}
689
+ * @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
567
690
  */
568
691
  function create_helper_state(base_name) {
569
692
  return {
570
693
  base_name,
571
694
  next_id: 0,
572
695
  helpers: [],
696
+ statics: [],
573
697
  };
574
698
  }
575
699
 
@@ -606,6 +730,18 @@ function collect_statement_bindings(statement, bindings) {
606
730
  ) {
607
731
  bindings.set(statement.id.name, statement.id);
608
732
  }
733
+
734
+ // Statement-level lazy assignment: `&[x] = expr;` introduces `x` as a binding.
735
+ if (
736
+ statement.type === 'ExpressionStatement' &&
737
+ statement.expression?.type === 'AssignmentExpression' &&
738
+ statement.expression.operator === '=' &&
739
+ (statement.expression.left?.type === 'ObjectPattern' ||
740
+ statement.expression.left?.type === 'ArrayPattern') &&
741
+ statement.expression.left.lazy
742
+ ) {
743
+ collect_pattern_bindings(statement.expression.left, bindings);
744
+ }
609
745
  }
610
746
 
611
747
  /**
@@ -649,6 +785,93 @@ function collect_pattern_bindings(pattern, bindings) {
649
785
  }
650
786
  }
651
787
 
788
+ /**
789
+ * Check if a node references any of the given scope bindings.
790
+ * Used to determine if a JSX element is static and can be hoisted to module level.
791
+ *
792
+ * @param {any} node
793
+ * @param {Map<string, AST.Identifier>} scope_bindings
794
+ * @returns {boolean}
795
+ */
796
+ function references_scope_bindings(node, scope_bindings) {
797
+ if (!node || typeof node !== 'object') return false;
798
+ if (scope_bindings.size === 0) return false;
799
+
800
+ if (node.type === 'Identifier') {
801
+ return scope_bindings.has(node.name);
802
+ }
803
+
804
+ // JSXIdentifier is a variable reference when capitalized (tag name like <MyComponent />)
805
+ // or when it's the object of a JSXMemberExpression (e.g. ui in <ui.Button />)
806
+ if (node.type === 'JSXIdentifier') {
807
+ return scope_bindings.has(node.name);
808
+ }
809
+
810
+ if (Array.isArray(node)) {
811
+ return node.some((child) => references_scope_bindings(child, scope_bindings));
812
+ }
813
+
814
+ for (const key of Object.keys(node)) {
815
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
816
+
817
+ // Skip non-computed, non-shorthand property keys (they are labels, not references)
818
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
819
+
820
+ // Skip non-computed member expression property access
821
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
822
+
823
+ // Skip JSXMemberExpression property (e.g. Button in <Icons.Button /> is a label, not a reference)
824
+ if (key === 'property' && node.type === 'JSXMemberExpression') continue;
825
+
826
+ // Skip JSXAttribute names — they are attribute labels, not variable references
827
+ if (key === 'name' && node.type === 'JSXAttribute') continue;
828
+
829
+ if (references_scope_bindings(node[key], scope_bindings)) return true;
830
+ }
831
+
832
+ return false;
833
+ }
834
+
835
+ /**
836
+ * Hoist static JSX elements from render_nodes to module level.
837
+ * A JSX element is static if it doesn't reference any component-scope bindings.
838
+ * Hoisting prevents React from recreating the element on every render, allowing
839
+ * the reconciler to skip diffing when it sees the same element identity.
840
+ *
841
+ * @param {any[]} render_nodes
842
+ * @param {TransformContext} transform_context
843
+ */
844
+ function hoist_static_render_nodes(render_nodes, transform_context) {
845
+ if (!transform_context.helper_state) return;
846
+
847
+ for (let i = 0; i < render_nodes.length; i++) {
848
+ const node = render_nodes[i];
849
+ if (node.type !== 'JSXElement') continue;
850
+ if (references_scope_bindings(node, transform_context.available_bindings)) continue;
851
+
852
+ const name = create_helper_name(transform_context.helper_state, 'static');
853
+ const id = create_generated_identifier(name);
854
+
855
+ transform_context.helper_state.statics.push(
856
+ /** @type {any} */ ({
857
+ type: 'VariableDeclaration',
858
+ kind: 'const',
859
+ declarations: [
860
+ {
861
+ type: 'VariableDeclarator',
862
+ id,
863
+ init: node,
864
+ metadata: { path: [] },
865
+ },
866
+ ],
867
+ metadata: { path: [] },
868
+ }),
869
+ );
870
+
871
+ render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
872
+ }
873
+ }
874
+
652
875
  /**
653
876
  * @param {AST.Identifier} identifier
654
877
  * @returns {AST.Identifier}
@@ -683,9 +906,11 @@ function create_null_literal() {
683
906
  function expand_component_helpers(program) {
684
907
  program.body = program.body.flatMap((statement) => {
685
908
  if (statement.type === 'FunctionDeclaration') {
686
- const helpers = /** @type {any} */ (statement.metadata)?.generated_helpers;
687
- if (helpers?.length) {
688
- return [...helpers, statement];
909
+ const meta = /** @type {any} */ (statement.metadata);
910
+ const statics = meta?.generated_statics || [];
911
+ const helpers = meta?.generated_helpers || [];
912
+ if (statics.length || helpers.length) {
913
+ return [...statics, ...helpers, statement];
689
914
  }
690
915
  }
691
916
 
@@ -694,9 +919,11 @@ function expand_component_helpers(program) {
694
919
  statement.type === 'ExportDefaultDeclaration') &&
695
920
  statement.declaration?.type === 'FunctionDeclaration'
696
921
  ) {
697
- const helpers = /** @type {any} */ (statement.declaration.metadata)?.generated_helpers;
698
- if (helpers?.length) {
699
- return [...helpers, statement];
922
+ const meta = /** @type {any} */ (statement.declaration.metadata);
923
+ const statics = meta?.generated_statics || [];
924
+ const helpers = meta?.generated_helpers || [];
925
+ if (statics.length || helpers.length) {
926
+ return [...statics, ...helpers, statement];
700
927
  }
701
928
  }
702
929
 
@@ -775,176 +1002,6 @@ function create_component_lone_return_if_statement(node, render_nodes) {
775
1002
  );
776
1003
  }
777
1004
 
778
- /**
779
- * Mark every selector inside the stylesheet as "used" so `renderStylesheets`
780
- * does not comment it out. We skip Ripple's selector-pruning pass because
781
- * React component boundaries are dynamic — any selector authored inside the
782
- * component's `<style>` block is considered intentional.
783
- *
784
- * @param {any} stylesheet
785
- * @returns {any}
786
- */
787
- function prepare_stylesheet_for_render(stylesheet) {
788
- walk(stylesheet, null, {
789
- _(node, { next }) {
790
- if (node && node.metadata && typeof node.metadata === 'object') {
791
- node.metadata.used = true;
792
- if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
793
- node.metadata.scoped = true;
794
- }
795
- }
796
- return next();
797
- },
798
- });
799
- return stylesheet;
800
- }
801
-
802
- /**
803
- * @param {any} node
804
- * @returns {boolean}
805
- */
806
- function is_style_element(node) {
807
- return (
808
- node &&
809
- node.type === 'Element' &&
810
- node.id &&
811
- node.id.type === 'Identifier' &&
812
- node.id.name === 'style'
813
- );
814
- }
815
-
816
- /**
817
- * @param {any} node
818
- * @returns {boolean}
819
- */
820
- function is_composite_element(node) {
821
- if (!node || node.type !== 'Element' || !node.id) {
822
- return false;
823
- }
824
-
825
- if (node.id.type === 'Identifier') {
826
- return /^[A-Z]/.test(node.id.name);
827
- }
828
-
829
- return node.id.type === 'MemberExpression';
830
- }
831
-
832
- /**
833
- * Recursively walk Element nodes within a component body and add the hash
834
- * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
835
- *
836
- * @param {any} node
837
- * @param {string} hash
838
- * @returns {any}
839
- */
840
- function annotate_with_hash(node, hash) {
841
- if (!node || typeof node !== 'object') return node;
842
- if (
843
- node.type === 'Component' ||
844
- node.type === 'FunctionDeclaration' ||
845
- node.type === 'FunctionExpression' ||
846
- node.type === 'ArrowFunctionExpression'
847
- ) {
848
- return node;
849
- }
850
-
851
- if (node.type === 'Element') {
852
- if (!is_style_element(node) && !is_composite_element(node)) {
853
- add_hash_class(node, hash);
854
- }
855
- if (Array.isArray(node.children)) {
856
- node.children = node.children
857
- .filter((/** @type {any} */ child) => !is_style_element(child))
858
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
859
- }
860
- return node;
861
- }
862
-
863
- for (const key of Object.keys(node)) {
864
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
865
- continue;
866
- }
867
-
868
- const value = node[key];
869
- if (Array.isArray(value)) {
870
- node[key] = value.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
871
- } else if (value && typeof value === 'object') {
872
- node[key] = annotate_with_hash(value, hash);
873
- }
874
- }
875
-
876
- return node;
877
- }
878
-
879
- /**
880
- * @param {any} component
881
- * @param {string} hash
882
- * @returns {void}
883
- */
884
- function annotate_component_with_hash(component, hash) {
885
- /** @type {any[]} */
886
- const body = component.body;
887
- component.body = body
888
- .filter((/** @type {any} */ child) => !is_style_element(child))
889
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
890
- }
891
-
892
- /**
893
- * Ensure the element carries a `class` attribute containing the scoping hash.
894
- * @param {any} element
895
- * @param {string} hash
896
- */
897
- function add_hash_class(element, hash) {
898
- const attrs = element.attributes || (element.attributes = []);
899
- const existing = attrs.find(
900
- (/** @type {any} */ a) =>
901
- a.type === 'Attribute' &&
902
- a.name &&
903
- a.name.type === 'Identifier' &&
904
- (a.name.name === 'class' || a.name.name === 'className'),
905
- );
906
-
907
- if (!existing) {
908
- attrs.push({
909
- type: 'Attribute',
910
- name: { type: 'Identifier', name: 'class' },
911
- value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
912
- });
913
- return;
914
- }
915
-
916
- const value = existing.value;
917
- if (!value) {
918
- existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
919
- return;
920
- }
921
-
922
- if (value.type === 'Literal' && typeof value.value === 'string') {
923
- const merged = `${value.value} ${hash}`;
924
- existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
925
- return;
926
- }
927
-
928
- // Dynamic expression. Concatenate at runtime via template literal.
929
- const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
930
- existing.value = {
931
- type: 'TemplateLiteral',
932
- expressions: [expression],
933
- quasis: [
934
- {
935
- type: 'TemplateElement',
936
- value: { raw: '', cooked: '' },
937
- tail: false,
938
- },
939
- {
940
- type: 'TemplateElement',
941
- value: { raw: ` ${hash}`, cooked: ` ${hash}` },
942
- tail: true,
943
- },
944
- ],
945
- };
946
- }
947
-
948
1005
  /**
949
1006
  * @param {any} node
950
1007
  * @returns {boolean}
@@ -959,6 +1016,10 @@ function is_jsx_child(node) {
959
1016
  t === 'JSXText' ||
960
1017
  t === 'Tsx' ||
961
1018
  t === 'TsxCompat' ||
1019
+ t === 'Element' ||
1020
+ t === 'Text' ||
1021
+ t === 'TSRXExpression' ||
1022
+ t === 'Html' ||
962
1023
  t === 'IfStatement' ||
963
1024
  t === 'ForOfStatement' ||
964
1025
  t === 'SwitchStatement' ||
@@ -973,6 +1034,11 @@ function is_jsx_child(node) {
973
1034
  */
974
1035
  function to_jsx_element(node, transform_context) {
975
1036
  if (node.type === 'JSXElement') return node;
1037
+ if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
1038
+ throw new Error(
1039
+ '`{html ...}` is not supported on the React target. Use `dangerouslySetInnerHTML={{ __html: ... }}` as an element attribute instead.',
1040
+ );
1041
+ }
976
1042
  if (is_dynamic_element_id(node.id)) {
977
1043
  return dynamic_element_to_jsx_child(node, transform_context);
978
1044
  }
@@ -1144,11 +1210,17 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1144
1210
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1145
1211
  source_node,
1146
1212
  );
1213
+ const helper_bindings = Array.from(transform_context.available_bindings.values());
1214
+
1215
+ // Save and isolate bindings for the helper body
1216
+ const saved_bindings = transform_context.available_bindings;
1217
+ transform_context.available_bindings = new Map(saved_bindings);
1218
+
1147
1219
  const helper_fn = set_loc(
1148
1220
  /** @type {any} */ ({
1149
1221
  type: 'FunctionDeclaration',
1150
1222
  id: helper_id,
1151
- params: [],
1223
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1152
1224
  body: {
1153
1225
  type: 'BlockStatement',
1154
1226
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1165,6 +1237,19 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1165
1237
  source_node,
1166
1238
  );
1167
1239
 
1240
+ // Restore bindings
1241
+ transform_context.available_bindings = saved_bindings;
1242
+
1243
+ // Register helper for hoisting to module level
1244
+ if (transform_context.helper_state) {
1245
+ transform_context.helper_state.helpers.push(helper_fn);
1246
+
1247
+ return to_jsx_expression_container(
1248
+ /** @type {any} */ (create_helper_component_element(helper_id, helper_bindings, source_node)),
1249
+ source_node,
1250
+ );
1251
+ }
1252
+
1168
1253
  return to_jsx_expression_container(
1169
1254
  /** @type {any} */ ({
1170
1255
  type: 'CallExpression',
@@ -1177,7 +1262,7 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
1177
1262
  helper_fn,
1178
1263
  {
1179
1264
  type: 'ReturnStatement',
1180
- argument: create_helper_component_element(helper_id, [], source_node),
1265
+ argument: create_helper_component_element(helper_id, helper_bindings, source_node),
1181
1266
  metadata: { path: [] },
1182
1267
  },
1183
1268
  ],
@@ -1206,8 +1291,10 @@ function create_local_statement_component_name(transform_context) {
1206
1291
  }
1207
1292
 
1208
1293
  /**
1209
- * Wraps a list of body nodes into a locally-declared component and returns
1210
- * statements that declare the component then return `<ComponentName />`.
1294
+ * Wraps a list of body nodes into a component and returns
1295
+ * statements that return `<ComponentName prop1={prop1} ... />`.
1296
+ * The component is hoisted to module level via helper_state to avoid
1297
+ * recreating the component identity on every render.
1211
1298
  * Used when a control flow branch contains hook calls that must be moved
1212
1299
  * into their own component boundary to satisfy the Rules of Hooks.
1213
1300
  *
@@ -1222,12 +1309,17 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1222
1309
  create_generated_identifier(create_local_statement_component_name(transform_context)),
1223
1310
  source_node,
1224
1311
  );
1312
+ const helper_bindings = Array.from(transform_context.available_bindings.values());
1313
+
1314
+ // Save and isolate bindings for the helper body
1315
+ const saved_bindings = transform_context.available_bindings;
1316
+ transform_context.available_bindings = new Map(saved_bindings);
1225
1317
 
1226
1318
  const helper_fn = set_loc(
1227
1319
  /** @type {any} */ ({
1228
1320
  type: 'FunctionDeclaration',
1229
1321
  id: helper_id,
1230
- params: [],
1322
+ params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
1231
1323
  body: {
1232
1324
  type: 'BlockStatement',
1233
1325
  body: build_render_statements(body_nodes, true, transform_context),
@@ -1244,7 +1336,19 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1244
1336
  source_node,
1245
1337
  );
1246
1338
 
1247
- const component_element = create_helper_component_element(helper_id, [], source_node);
1339
+ // Restore bindings
1340
+ transform_context.available_bindings = saved_bindings;
1341
+
1342
+ // Register helper for hoisting to module level
1343
+ if (transform_context.helper_state) {
1344
+ transform_context.helper_state.helpers.push(helper_fn);
1345
+ }
1346
+
1347
+ const component_element = create_helper_component_element(
1348
+ helper_id,
1349
+ helper_bindings,
1350
+ source_node,
1351
+ );
1248
1352
 
1249
1353
  if (key_expression) {
1250
1354
  component_element.openingElement.attributes.push(
@@ -1257,8 +1361,20 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
1257
1361
  );
1258
1362
  }
1259
1363
 
1364
+ // When helper_state is null (no enclosing component context), inline the
1365
+ // helper via an IIFE so the function declaration isn't silently dropped.
1366
+ if (!transform_context.helper_state) {
1367
+ return [
1368
+ helper_fn,
1369
+ {
1370
+ type: 'ReturnStatement',
1371
+ argument: component_element,
1372
+ metadata: { path: [] },
1373
+ },
1374
+ ];
1375
+ }
1376
+
1260
1377
  return [
1261
- helper_fn,
1262
1378
  {
1263
1379
  type: 'ReturnStatement',
1264
1380
  argument: component_element,
@@ -1307,6 +1423,10 @@ function to_jsx_child(node, transform_context) {
1307
1423
  return to_jsx_expression_container(to_text_expression(node.expression, node), node);
1308
1424
  case 'TSRXExpression':
1309
1425
  return to_jsx_expression_container(node.expression, node);
1426
+ case 'Html':
1427
+ throw new Error(
1428
+ '`{html ...}` is not supported on the React target. Use `dangerouslySetInnerHTML={{ __html: ... }}` as an element attribute instead.',
1429
+ );
1310
1430
  case 'IfStatement':
1311
1431
  return if_statement_to_jsx_child(node, transform_context);
1312
1432
  case 'ForOfStatement':
@@ -1411,6 +1531,20 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1411
1531
  const has_hooks = body_contains_top_level_hook_call(loop_body);
1412
1532
  const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
1413
1533
 
1534
+ // Add loop params to available bindings so hoisted helpers receive them as props
1535
+ const saved_bindings = transform_context.available_bindings;
1536
+ transform_context.available_bindings = new Map(saved_bindings);
1537
+ for (const param of loop_params) {
1538
+ collect_pattern_bindings(param, transform_context.available_bindings);
1539
+ }
1540
+
1541
+ const body_statements = has_hooks
1542
+ ? hook_safe_render_statements(loop_body, key_expression, transform_context)
1543
+ : build_render_statements(loop_body, true, transform_context);
1544
+
1545
+ // Restore bindings
1546
+ transform_context.available_bindings = saved_bindings;
1547
+
1414
1548
  return to_jsx_expression_container(
1415
1549
  /** @type {any} */ ({
1416
1550
  type: 'CallExpression',
@@ -1428,9 +1562,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1428
1562
  params: loop_params,
1429
1563
  body: /** @type {any} */ ({
1430
1564
  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),
1565
+ body: body_statements,
1434
1566
  metadata: { path: [] },
1435
1567
  }),
1436
1568
  async: false,
@@ -1572,6 +1704,15 @@ function try_statement_to_jsx_child(node, transform_context) {
1572
1704
  }
1573
1705
 
1574
1706
  const catch_body_nodes = handler.body.body || [];
1707
+
1708
+ // Add catch params to available_bindings so static hoisting
1709
+ // correctly identifies references to err/reset as non-static
1710
+ const saved_catch_bindings = transform_context.available_bindings;
1711
+ transform_context.available_bindings = new Map(saved_catch_bindings);
1712
+ for (const param of catch_params) {
1713
+ collect_pattern_bindings(param, transform_context.available_bindings);
1714
+ }
1715
+
1575
1716
  const fallback_fn = {
1576
1717
  type: 'ArrowFunctionExpression',
1577
1718
  params: catch_params,
@@ -1586,6 +1727,8 @@ function try_statement_to_jsx_child(node, transform_context) {
1586
1727
  metadata: { path: [] },
1587
1728
  };
1588
1729
 
1730
+ transform_context.available_bindings = saved_catch_bindings;
1731
+
1589
1732
  result = create_jsx_element(
1590
1733
  'TsrxErrorBoundary',
1591
1734
  [