@tsrx/react 0.0.3 → 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.3",
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,7 +4,16 @@
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 {{
@@ -14,6 +23,7 @@ import { renderStylesheets, setLocation } from '@tsrx/core';
14
23
  * helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
15
24
  * available_bindings: Map<string, AST.Identifier>,
16
25
  * lazy_next_id: number,
26
+ * current_css_hash: string | null,
17
27
  * }} TransformContext
18
28
  */
19
29
 
@@ -49,8 +59,11 @@ export function transform(ast, source, filename) {
49
59
  helper_state: null,
50
60
  available_bindings: new Map(),
51
61
  lazy_next_id: 0,
62
+ current_css_hash: null,
52
63
  };
53
64
 
65
+ preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
66
+
54
67
  walk(/** @type {any} */ (ast), transform_context, {
55
68
  Component(node, { next, state }) {
56
69
  const as_any = /** @type {any} */ (node);
@@ -74,7 +87,9 @@ export function transform(ast, source, filename) {
74
87
  const helper_state = create_helper_state(as_any.id?.name || 'Component');
75
88
  const saved_helper_state = state.helper_state;
76
89
  const saved_bindings = state.available_bindings;
90
+ const saved_css_hash = state.current_css_hash;
77
91
  state.helper_state = helper_state;
92
+ state.current_css_hash = as_any.css ? as_any.css.hash : null;
78
93
 
79
94
  // Pre-collect component body bindings (params + top-level statements)
80
95
  // so that Element children processed during the bottom-up walk can see
@@ -97,6 +112,7 @@ export function transform(ast, source, filename) {
97
112
  // Restore context
98
113
  state.helper_state = saved_helper_state;
99
114
  state.available_bindings = saved_bindings;
115
+ state.current_css_hash = saved_css_hash;
100
116
 
101
117
  return /** @type {any} */ (component_to_function_declaration(inner, state, helper_state));
102
118
  },
@@ -127,12 +143,30 @@ export function transform(ast, source, filename) {
127
143
  const inner = /** @type {any} */ (next() ?? node);
128
144
  return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
129
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
+ },
130
156
  });
131
157
 
132
158
  const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
133
159
  inject_try_imports(expanded, transform_context);
134
160
 
135
- 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(), {
136
170
  sourceMapSource: filename,
137
171
  sourceMapContent: source,
138
172
  });
@@ -147,508 +181,7 @@ export function transform(ast, source, filename) {
147
181
  }
148
182
  : null;
149
183
 
150
- return { ast: expanded, code: result.code, map: result.map, css };
151
- }
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
- });
184
+ return { ast: final_program, code: result.code, map: result.map, css };
652
185
  }
653
186
 
654
187
  /**
@@ -1197,6 +730,18 @@ function collect_statement_bindings(statement, bindings) {
1197
730
  ) {
1198
731
  bindings.set(statement.id.name, statement.id);
1199
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
+ }
1200
745
  }
1201
746
 
1202
747
  /**
@@ -1457,176 +1002,6 @@ function create_component_lone_return_if_statement(node, render_nodes) {
1457
1002
  );
1458
1003
  }
1459
1004
 
1460
- /**
1461
- * Mark every selector inside the stylesheet as "used" so `renderStylesheets`
1462
- * does not comment it out. We skip Ripple's selector-pruning pass because
1463
- * React component boundaries are dynamic — any selector authored inside the
1464
- * component's `<style>` block is considered intentional.
1465
- *
1466
- * @param {any} stylesheet
1467
- * @returns {any}
1468
- */
1469
- function prepare_stylesheet_for_render(stylesheet) {
1470
- walk(stylesheet, null, {
1471
- _(node, { next }) {
1472
- if (node && node.metadata && typeof node.metadata === 'object') {
1473
- node.metadata.used = true;
1474
- if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
1475
- node.metadata.scoped = true;
1476
- }
1477
- }
1478
- return next();
1479
- },
1480
- });
1481
- return stylesheet;
1482
- }
1483
-
1484
- /**
1485
- * @param {any} node
1486
- * @returns {boolean}
1487
- */
1488
- function is_style_element(node) {
1489
- return (
1490
- node &&
1491
- node.type === 'Element' &&
1492
- node.id &&
1493
- node.id.type === 'Identifier' &&
1494
- node.id.name === 'style'
1495
- );
1496
- }
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
-
1514
- /**
1515
- * Recursively walk Element nodes within a component body and add the hash
1516
- * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
1517
- *
1518
- * @param {any} node
1519
- * @param {string} hash
1520
- * @returns {any}
1521
- */
1522
- function annotate_with_hash(node, hash) {
1523
- if (!node || typeof node !== 'object') return node;
1524
- if (
1525
- node.type === 'Component' ||
1526
- node.type === 'FunctionDeclaration' ||
1527
- node.type === 'FunctionExpression' ||
1528
- node.type === 'ArrowFunctionExpression'
1529
- ) {
1530
- return node;
1531
- }
1532
-
1533
- if (node.type === 'Element') {
1534
- if (!is_style_element(node) && !is_composite_element(node)) {
1535
- add_hash_class(node, hash);
1536
- }
1537
- if (Array.isArray(node.children)) {
1538
- node.children = node.children
1539
- .filter((/** @type {any} */ child) => !is_style_element(child))
1540
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
1541
- }
1542
- return node;
1543
- }
1544
-
1545
- for (const key of Object.keys(node)) {
1546
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
1547
- continue;
1548
- }
1549
-
1550
- const value = node[key];
1551
- if (Array.isArray(value)) {
1552
- node[key] = value.map((/** @type {any} */ child) => annotate_with_hash(child, hash));
1553
- } else if (value && typeof value === 'object') {
1554
- node[key] = annotate_with_hash(value, hash);
1555
- }
1556
- }
1557
-
1558
- return node;
1559
- }
1560
-
1561
- /**
1562
- * @param {any} component
1563
- * @param {string} hash
1564
- * @returns {void}
1565
- */
1566
- function annotate_component_with_hash(component, hash) {
1567
- /** @type {any[]} */
1568
- const body = component.body;
1569
- component.body = body
1570
- .filter((/** @type {any} */ child) => !is_style_element(child))
1571
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash));
1572
- }
1573
-
1574
- /**
1575
- * Ensure the element carries a `class` attribute containing the scoping hash.
1576
- * @param {any} element
1577
- * @param {string} hash
1578
- */
1579
- function add_hash_class(element, hash) {
1580
- const attrs = element.attributes || (element.attributes = []);
1581
- const existing = attrs.find(
1582
- (/** @type {any} */ a) =>
1583
- a.type === 'Attribute' &&
1584
- a.name &&
1585
- a.name.type === 'Identifier' &&
1586
- (a.name.name === 'class' || a.name.name === 'className'),
1587
- );
1588
-
1589
- if (!existing) {
1590
- attrs.push({
1591
- type: 'Attribute',
1592
- name: { type: 'Identifier', name: 'class' },
1593
- value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
1594
- });
1595
- return;
1596
- }
1597
-
1598
- const value = existing.value;
1599
- if (!value) {
1600
- existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
1601
- return;
1602
- }
1603
-
1604
- if (value.type === 'Literal' && typeof value.value === 'string') {
1605
- const merged = `${value.value} ${hash}`;
1606
- existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
1607
- return;
1608
- }
1609
-
1610
- // Dynamic expression. Concatenate at runtime via template literal.
1611
- const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
1612
- existing.value = {
1613
- type: 'TemplateLiteral',
1614
- expressions: [expression],
1615
- quasis: [
1616
- {
1617
- type: 'TemplateElement',
1618
- value: { raw: '', cooked: '' },
1619
- tail: false,
1620
- },
1621
- {
1622
- type: 'TemplateElement',
1623
- value: { raw: ` ${hash}`, cooked: ` ${hash}` },
1624
- tail: true,
1625
- },
1626
- ],
1627
- };
1628
- }
1629
-
1630
1005
  /**
1631
1006
  * @param {any} node
1632
1007
  * @returns {boolean}
@@ -1641,6 +1016,10 @@ function is_jsx_child(node) {
1641
1016
  t === 'JSXText' ||
1642
1017
  t === 'Tsx' ||
1643
1018
  t === 'TsxCompat' ||
1019
+ t === 'Element' ||
1020
+ t === 'Text' ||
1021
+ t === 'TSRXExpression' ||
1022
+ t === 'Html' ||
1644
1023
  t === 'IfStatement' ||
1645
1024
  t === 'ForOfStatement' ||
1646
1025
  t === 'SwitchStatement' ||
@@ -1655,6 +1034,11 @@ function is_jsx_child(node) {
1655
1034
  */
1656
1035
  function to_jsx_element(node, transform_context) {
1657
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
+ }
1658
1042
  if (is_dynamic_element_id(node.id)) {
1659
1043
  return dynamic_element_to_jsx_child(node, transform_context);
1660
1044
  }
@@ -2039,6 +1423,10 @@ function to_jsx_child(node, transform_context) {
2039
1423
  return to_jsx_expression_container(to_text_expression(node.expression, node), node);
2040
1424
  case 'TSRXExpression':
2041
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
+ );
2042
1430
  case 'IfStatement':
2043
1431
  return if_statement_to_jsx_child(node, transform_context);
2044
1432
  case 'ForOfStatement':