@tsrx/core 0.1.31 → 0.1.33

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.
@@ -99,38 +99,6 @@ function get_lazy_pattern_mapping_range(pattern) {
99
99
  };
100
100
  }
101
101
 
102
- /**
103
- * Synthesize an object-shaped annotation for untyped lazy object params so the
104
- * virtual TSX can expose prop names to TypeScript completions.
105
- *
106
- * @param {any} pattern
107
- * @returns {any | null}
108
- */
109
- function create_lazy_object_type_annotation(pattern) {
110
- if (pattern.type !== 'ObjectPattern') return null;
111
-
112
- const members = [];
113
- for (const prop of pattern.properties || []) {
114
- if (prop.type === 'RestElement' || prop.computed) continue;
115
-
116
- const key = prop.key;
117
- if (key.type !== 'Identifier' && key.type !== 'Literal') continue;
118
-
119
- const member_key =
120
- key.type === 'Identifier'
121
- ? create_generated_identifier(key.name, key)
122
- : set_source_location({ ...key, metadata: { path: [] } }, key);
123
-
124
- members.push(
125
- b.ts_property_signature(member_key, b.ts_type_annotation(b.ts_keyword_type('any'))),
126
- );
127
- }
128
-
129
- if (members.length === 0) return null;
130
-
131
- return b.ts_type_annotation(b.ts_type_literal(members));
132
- }
133
-
134
102
  /**
135
103
  * @param {any} node
136
104
  * @returns {string | null}
@@ -348,12 +316,14 @@ function visit_topmost_lazy_patterns(pattern, visit) {
348
316
 
349
317
  /**
350
318
  * Build the replacement identifier for a lazy pattern. When `is_top` is true
351
- * (the pattern is itself a function parameter) we attach the original
352
- * `typeAnnotation`, synthesize an object-shaped annotation for untyped object
353
- * params so TypeScript sees prop names, and register source-mapping info.
354
- * Nested replacements (inside a non-lazy outer destructure) can't carry an
355
- * inline type annotation that's not valid syntax so they get a plain
356
- * identifier with just source-range info.
319
+ * (the pattern is itself a function parameter) we carry over the author's
320
+ * `typeAnnotation` if one was written and register source-mapping info. An
321
+ * untyped lazy object param gets NO synthesized annotation: since the source
322
+ * specified no type, the generated param is left implicitly `any` rather than
323
+ * being given a fabricated `{ : any }` object type. Nested replacements
324
+ * (inside a non-lazy outer destructure) can't carry an inline type annotation —
325
+ * that's not valid syntax — so they get a plain identifier with just source-range
326
+ * info.
357
327
  *
358
328
  * @param {any} pattern
359
329
  * @param {boolean} is_top
@@ -372,9 +342,6 @@ function build_lazy_id_for_pattern(pattern, is_top) {
372
342
  if (!is_top) return lazy_id;
373
343
  if (pattern.typeAnnotation) {
374
344
  lazy_id.typeAnnotation = pattern.typeAnnotation;
375
- } else {
376
- const type_annotation = create_lazy_object_type_annotation(pattern);
377
- if (type_annotation) lazy_id.typeAnnotation = type_annotation;
378
345
  }
379
346
  set_lazy_param_binding_mappings(lazy_id, pattern);
380
347
  return lazy_id;
@@ -511,6 +478,53 @@ export function preallocate_lazy_ids(root, context) {
511
478
  visit(root);
512
479
  }
513
480
 
481
+ /**
482
+ * Convert an ESTree member-access chain (`__lazy0.Item`, `__lazy0.a.b`) into the
483
+ * equivalent JSX element-name node (a `JSXIdentifier` or `JSXMemberExpression`),
484
+ * so a lazy binding used as a component/element name (`<Item>`) can be rewritten
485
+ * to `<__lazy0.Item>`. Returns null for a computed access (`__lazy0[0]`, from an
486
+ * array destructure), which has no JSX-name form.
487
+ *
488
+ * @param {any} expr
489
+ * @param {any} [source]
490
+ * @returns {any | null}
491
+ */
492
+ function estree_member_to_jsx_name(expr, source) {
493
+ if (expr.type === 'Identifier') {
494
+ return set_source_location(b.jsx_id(expr.name), source);
495
+ }
496
+ if (expr.type === 'MemberExpression' && !expr.computed && expr.property?.type === 'Identifier') {
497
+ const object = estree_member_to_jsx_name(expr.object, source);
498
+ if (!object) return null;
499
+ return set_source_location(b.jsx_member(object, b.jsx_id(expr.property.name)), source);
500
+ }
501
+ return null;
502
+ }
503
+
504
+ /**
505
+ * If a JSX element/component name references a lazy binding, return the rewritten
506
+ * JSX name (`<Item>` → `<__lazy0.Item>`, `<Item.Sub>` → `<__lazy0.Item.Sub>`).
507
+ * Returns null when the name does not reference a lazy binding (or the access has
508
+ * no JSX-name form), so the caller leaves the original name untouched.
509
+ *
510
+ * @param {any} name
511
+ * @param {Map<string, LazyBinding>} lazy_bindings
512
+ * @returns {any | null}
513
+ */
514
+ function rewrite_lazy_jsx_name(name, lazy_bindings) {
515
+ if (!name) return null;
516
+ if (name.type === 'JSXIdentifier') {
517
+ const binding = lazy_bindings.get(name.name);
518
+ if (!binding) return null;
519
+ return estree_member_to_jsx_name(binding.read(name), name);
520
+ }
521
+ if (name.type === 'JSXMemberExpression') {
522
+ const new_object = rewrite_lazy_jsx_name(name.object, lazy_bindings);
523
+ return new_object ? { ...name, object: new_object } : null;
524
+ }
525
+ return null;
526
+ }
527
+
514
528
  /**
515
529
  * Recursively rewrite lazy-binding references in `node`.
516
530
  *
@@ -669,16 +683,36 @@ export function apply_lazy_transforms(node, lazy_bindings) {
669
683
  }
670
684
 
671
685
  if (node.type === 'SwitchStatement') {
686
+ // All case consequents share one lexical block scope, and `@switch`
687
+ // case bodies are flattened (the `{ … }` wrapper is dropped), so a
688
+ // `let &[x] = …` / `let &{ … } = …` declared in a case body is scoped to
689
+ // the whole switch block. Collect names and lazy bindings across every
690
+ // consequent — like the BlockStatement handler does for a block body — so
691
+ // references in any case resolve to the generated id, and an inner
692
+ // declaration shadowing an outer lazy name is dropped.
693
+ const all_consequents = node.cases.flatMap(
694
+ (/** @type {any} */ switch_case) => switch_case.consequent,
695
+ );
696
+ const block_shadowed = collect_block_shadowed_names(all_consequents, lazy_bindings);
697
+ const after_shadow =
698
+ block_shadowed.size > 0 ? remove_shadowed(lazy_bindings, block_shadowed) : lazy_bindings;
699
+
700
+ /** @type {Map<string, LazyBinding>} */
701
+ const block_lazy = new Map();
702
+ collect_lazy_bindings_from_statements(all_consequents, block_lazy);
703
+ const effective_bindings =
704
+ block_lazy.size > 0 ? new Map([...after_shadow, ...block_lazy]) : after_shadow;
705
+
672
706
  let changed = false;
673
- const new_discriminant = apply_lazy_transforms(node.discriminant, lazy_bindings);
707
+ // The discriminant is evaluated before any case body runs, so it sees the
708
+ // bindings visible at the switch (outer, minus inner shadows), not a lazy
709
+ // binding declared inside a case body.
710
+ const new_discriminant = apply_lazy_transforms(node.discriminant, after_shadow);
674
711
  if (new_discriminant !== node.discriminant) changed = true;
675
712
  const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
676
- const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
677
- const effective_bindings =
678
- case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
679
713
  let case_changed = false;
680
714
  const new_test = switch_case.test
681
- ? apply_lazy_transforms(switch_case.test, lazy_bindings)
715
+ ? apply_lazy_transforms(switch_case.test, effective_bindings)
682
716
  : null;
683
717
  if (new_test !== switch_case.test) case_changed = true;
684
718
  const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
@@ -823,6 +857,21 @@ export function apply_lazy_transforms(node, lazy_bindings) {
823
857
  // Skip VariableDeclarator id (already handled above).
824
858
  if (key === 'id' && node.type === 'VariableDeclarator') continue;
825
859
 
860
+ // A JSX element/component name that resolves to a lazy binding becomes a
861
+ // JSX member expression (`<Item>` → `<__lazy0.Item>`): the bound name is no
862
+ // longer a local, it is a property of the synthesized lazy source.
863
+ if (
864
+ key === 'name' &&
865
+ (node.type === 'JSXOpeningElement' || node.type === 'JSXClosingElement')
866
+ ) {
867
+ const jsx_name = rewrite_lazy_jsx_name(node[key], lazy_bindings);
868
+ if (jsx_name) {
869
+ clone[key] = jsx_name;
870
+ changed = true;
871
+ continue;
872
+ }
873
+ }
874
+
826
875
  const new_value = apply_lazy_transforms(node[key], lazy_bindings);
827
876
  if (new_value !== node[key]) {
828
877
  clone[key] = new_value;
@@ -368,6 +368,26 @@ export function literal(value, raw, loc_info) {
368
368
  return set_location(node, loc_info);
369
369
  }
370
370
 
371
+ /**
372
+ * Ripple template `Text` node wrapping a string literal of `value`. Used by the
373
+ * Ripple normalizer when lowering a JSX text child to a template Text node.
374
+ *
375
+ * Remove once Ripple transformers move to the parser's JSX AST
376
+ *
377
+ * @param {string} value
378
+ * @param {AST.NodeWithLocation} [loc_info]
379
+ * @returns {AST.Text}
380
+ */
381
+ export function text(value, loc_info) {
382
+ const node = /** @type {AST.Text} */ ({
383
+ type: 'Text',
384
+ expression: literal(value, JSON.stringify(value), loc_info),
385
+ metadata: { path: [] },
386
+ });
387
+
388
+ return set_location(node, loc_info);
389
+ }
390
+
371
391
  /**
372
392
  * @param {AST.Expression | AST.Super} object
373
393
  * @param {string | AST.Expression | AST.PrivateIdentifier} property
package/types/index.d.ts CHANGED
@@ -83,6 +83,7 @@ interface BaseNodeMetaData {
83
83
  commentContainerId?: number;
84
84
  parenthesized?: boolean;
85
85
  native_tsrx?: boolean;
86
+ tsrx_generated_wrapper?: boolean;
86
87
  native_tsrx_template_block?: boolean;
87
88
  dynamicElement?: boolean;
88
89
  templateMode?: 'script' | 'template';
@@ -137,6 +138,7 @@ type AcornTSNode<T> = Omit<T, 'parent' | 'loc' | 'range' | 'expression'> & {
137
138
 
138
139
  leadingComments?: AST.Comment[] | undefined;
139
140
  trailingComments?: AST.Comment[] | undefined;
141
+ append_into?: AST.Identifier;
140
142
  };
141
143
 
142
144
  interface FunctionLikeTS {
@@ -295,6 +297,8 @@ declare module 'estree' {
295
297
  TSAsExpression: TSAsExpression;
296
298
  }
297
299
 
300
+ type TraversableAstNode = AST.Node & Record<string, unknown>;
301
+
298
302
  // Ripple-normalized template node shapes. The core parser emits JSX-shaped
299
303
  // TSRX nodes; @tsrx/ripple creates these during its normalization pass.
300
304
  interface Attribute extends AST.BaseNode {
@@ -516,6 +520,8 @@ declare module 'estree' {
516
520
  metadata: BaseNodeMetaData;
517
521
 
518
522
  comments?: Comment[];
523
+
524
+ append_into?: AST.Identifier;
519
525
  }
520
526
 
521
527
  interface NodeWithLocation {