@tsrx/core 0.0.13 → 0.0.14

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.
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  /**
29
- * @typedef {{ source_name: string, read: () => any }} LazyBinding
29
+ * @typedef {{ source_name: string, read: (reference?: any) => any }} LazyBinding
30
30
  */
31
31
 
32
32
  /**
@@ -46,12 +46,169 @@ function generate_lazy_id(context) {
46
46
  return `__lazy${context.lazy_next_id++}`;
47
47
  }
48
48
 
49
+ /**
50
+ * @param {any} node
51
+ * @param {any} [loc_info]
52
+ * @returns {any}
53
+ */
54
+ function set_source_location(node, loc_info) {
55
+ if (loc_info?.loc) {
56
+ node.start = loc_info.start;
57
+ node.end = loc_info.end;
58
+ node.loc = loc_info.loc;
59
+ }
60
+ return node;
61
+ }
62
+
49
63
  /**
50
64
  * @param {string} name
65
+ * @param {any} [loc_info]
66
+ * @param {string} [source_name]
67
+ * @param {number} [source_length]
51
68
  * @returns {any}
52
69
  */
53
- function create_generated_identifier(name) {
54
- return /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
70
+ function create_generated_identifier(name, loc_info, source_name, source_length) {
71
+ const id = /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
72
+ if (source_name && source_name !== name) id.metadata.source_name = source_name;
73
+ if (source_length != null) id.metadata.source_length = source_length;
74
+ return set_source_location(id, loc_info);
75
+ }
76
+
77
+ /**
78
+ * @param {any} pattern
79
+ * @returns {{ start: number, end: number, loc: any, source_length: number } | null}
80
+ */
81
+ function get_lazy_pattern_mapping_range(pattern) {
82
+ if (!pattern.loc) return null;
83
+
84
+ const end = pattern.typeAnnotation?.start ?? pattern.end;
85
+ const end_loc = pattern.typeAnnotation?.loc?.start ?? pattern.loc.end;
86
+ return {
87
+ start: pattern.start,
88
+ end,
89
+ loc: {
90
+ start: pattern.loc.start,
91
+ end: end_loc,
92
+ },
93
+ source_length: end - pattern.start,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Synthesize an object-shaped annotation for untyped lazy object params so the
99
+ * virtual TSX can expose prop names to TypeScript completions.
100
+ *
101
+ * @param {any} pattern
102
+ * @returns {any | null}
103
+ */
104
+ function create_lazy_object_type_annotation(pattern) {
105
+ if (pattern.type !== 'ObjectPattern') return null;
106
+
107
+ const members = [];
108
+ for (const prop of pattern.properties || []) {
109
+ if (prop.type === 'RestElement' || prop.computed) continue;
110
+
111
+ const key = prop.key;
112
+ if (key.type !== 'Identifier' && key.type !== 'Literal') continue;
113
+
114
+ members.push({
115
+ type: 'TSPropertySignature',
116
+ key:
117
+ key.type === 'Identifier'
118
+ ? create_generated_identifier(key.name, key)
119
+ : set_source_location({ ...key, metadata: { path: [] } }, key),
120
+ computed: false,
121
+ optional: false,
122
+ readonly: false,
123
+ static: false,
124
+ kind: 'init',
125
+ typeAnnotation: {
126
+ type: 'TSTypeAnnotation',
127
+ typeAnnotation: {
128
+ type: 'TSAnyKeyword',
129
+ metadata: { path: [] },
130
+ },
131
+ metadata: { path: [] },
132
+ },
133
+ metadata: { path: [] },
134
+ });
135
+ }
136
+
137
+ if (members.length === 0) return null;
138
+
139
+ return {
140
+ type: 'TSTypeAnnotation',
141
+ typeAnnotation: {
142
+ type: 'TSTypeLiteral',
143
+ members,
144
+ metadata: { path: [] },
145
+ },
146
+ metadata: { path: [] },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * @param {any} node
152
+ * @returns {string | null}
153
+ */
154
+ function get_static_property_name(node) {
155
+ if (node.type === 'Identifier') return node.name;
156
+ if (node.type === 'Literal') return String(node.value);
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * @param {any} type_annotation
162
+ * @returns {Map<string, any>}
163
+ */
164
+ function get_type_property_keys(type_annotation) {
165
+ const keys = new Map();
166
+ const members = type_annotation?.typeAnnotation?.members;
167
+ if (!Array.isArray(members)) return keys;
168
+
169
+ for (const member of members) {
170
+ if (member.type !== 'TSPropertySignature' || !member.key) continue;
171
+ const name = get_static_property_name(member.key);
172
+ if (name != null && !keys.has(name)) keys.set(name, member.key);
173
+ }
174
+
175
+ return keys;
176
+ }
177
+
178
+ /**
179
+ * Store extra mappings from lazy object binding identifiers to generated type
180
+ * property keys. Parser diagnostics for duplicate bindings point at the binding
181
+ * names (`&{ a: value, value }`), while the virtual param only exposes object
182
+ * properties (`__lazy0: { a: ...; value: ... }`).
183
+ *
184
+ * @param {any} lazy_id
185
+ * @param {any} pattern
186
+ */
187
+ function set_lazy_param_binding_mappings(lazy_id, pattern) {
188
+ if (pattern.type !== 'ObjectPattern') return;
189
+
190
+ const type_keys = get_type_property_keys(lazy_id.typeAnnotation);
191
+ if (type_keys.size === 0) return;
192
+
193
+ const mappings = [];
194
+ for (const prop of pattern.properties || []) {
195
+ if (prop.type === 'RestElement' || prop.computed) continue;
196
+
197
+ const value = prop.value;
198
+ const actual = value.type === 'AssignmentPattern' ? value.left : value;
199
+ if (actual.type !== 'Identifier' || !actual.loc) continue;
200
+
201
+ const key_name = get_static_property_name(prop.key);
202
+ const generated = key_name == null ? null : type_keys.get(key_name);
203
+ if (generated?.loc) {
204
+ generated.metadata = { ...generated.metadata, disable_verification: true };
205
+ mappings.push({ source: actual, generated });
206
+ }
207
+ }
208
+
209
+ if (mappings.length > 0) {
210
+ lazy_id.metadata.lazy_param_binding_mappings = mappings;
211
+ }
55
212
  }
56
213
 
57
214
  /**
@@ -76,12 +233,13 @@ export function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
76
233
  const computed = prop.computed || key.type !== 'Identifier';
77
234
  lazy_bindings.set(actual.name, {
78
235
  source_name,
79
- read: () => ({
236
+ read: (reference) => ({
80
237
  type: 'MemberExpression',
81
238
  object: create_generated_identifier(source_name),
82
- property: computed
83
- ? { ...key }
84
- : { type: 'Identifier', name: key.name, metadata: { path: [] } },
239
+ property:
240
+ computed || key.type !== 'Identifier'
241
+ ? { ...key }
242
+ : create_generated_identifier(key.name, reference, reference?.name),
85
243
  computed,
86
244
  optional: false,
87
245
  metadata: { path: [] },
@@ -491,7 +649,7 @@ export function apply_lazy_transforms(node, lazy_bindings) {
491
649
  const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.left.name));
492
650
  return {
493
651
  ...node,
494
- left: binding.read(),
652
+ left: binding.read(node.left),
495
653
  right: apply_lazy_transforms(node.right, lazy_bindings),
496
654
  };
497
655
  }
@@ -502,7 +660,7 @@ export function apply_lazy_transforms(node, lazy_bindings) {
502
660
  lazy_bindings.has(node.argument.name)
503
661
  ) {
504
662
  const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.argument.name));
505
- return { ...node, argument: binding.read() };
663
+ return { ...node, argument: binding.read(node.argument) };
506
664
  }
507
665
 
508
666
  // Replace lazy variable declaration patterns with generated identifiers.
@@ -520,14 +678,14 @@ export function apply_lazy_transforms(node, lazy_bindings) {
520
678
  if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
521
679
  const binding = lazy_bindings.get(node.value.name);
522
680
  if (binding) {
523
- return { ...node, shorthand: false, value: binding.read() };
681
+ return { ...node, shorthand: false, value: binding.read(node.value) };
524
682
  }
525
683
  }
526
684
 
527
685
  // Bare identifier reference.
528
686
  if (node.type === 'Identifier' && lazy_bindings.has(node.name)) {
529
687
  const binding = /** @type {LazyBinding} */ (lazy_bindings.get(node.name));
530
- return binding.read();
688
+ return binding.read(node);
531
689
  }
532
690
 
533
691
  // JSXIdentifier is a label (component/element name), never a reference.
@@ -654,8 +812,22 @@ export function replace_lazy_params(params) {
654
812
  pattern.lazy &&
655
813
  pattern.metadata?.lazy_id
656
814
  ) {
657
- const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
658
- if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
815
+ const pattern_range = get_lazy_pattern_mapping_range(pattern);
816
+ const lazy_id = pattern_range
817
+ ? create_generated_identifier(
818
+ pattern.metadata.lazy_id,
819
+ pattern_range,
820
+ undefined,
821
+ pattern_range.source_length,
822
+ )
823
+ : create_generated_identifier(pattern.metadata.lazy_id);
824
+ if (pattern.typeAnnotation) {
825
+ lazy_id.typeAnnotation = pattern.typeAnnotation;
826
+ } else {
827
+ const type_annotation = create_lazy_object_type_annotation(pattern);
828
+ if (type_annotation) lazy_id.typeAnnotation = type_annotation;
829
+ }
830
+ set_lazy_param_binding_mappings(lazy_id, pattern);
659
831
  if (param.type === 'AssignmentPattern') return { ...param, left: lazy_id };
660
832
  return lazy_id;
661
833
  }
@@ -27,6 +27,7 @@
27
27
  loc: AST.SourceLocation;
28
28
  metadata: PluginActionOverrides;
29
29
  end_loc?: AST.SourceLocation;
30
+ sourceLength?: number;
30
31
  mappingData?: Partial<VolarCodeMapping['data']>;
31
32
  }} Token;
32
33
  @typedef {{
@@ -55,6 +56,15 @@ import {
55
56
  } from '../source-map-utils.js';
56
57
 
57
58
  const LABEL_TO_COMPONENT_REPLACE_REGEX = /(function|\((property|method)\))/;
59
+ const LAZY_PARAM_IDENTIFIER_REGEX = /^__lazy\d+$/;
60
+
61
+ /**
62
+ * @param {string} value
63
+ * @returns {string}
64
+ */
65
+ function escape_regex(value) {
66
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
+ }
58
68
 
59
69
  /**
60
70
  * @param {string} content
@@ -67,6 +77,51 @@ function replace_label_to_component(content) {
67
77
  });
68
78
  }
69
79
 
80
+ /**
81
+ * @param {string} lazy_id
82
+ * @param {(content: string) => string} [base_hover]
83
+ * @returns {(content: string) => string}
84
+ */
85
+ function create_lazy_param_hover_replacement(lazy_id, base_hover) {
86
+ const lazy_param_regex = new RegExp(`\\b${escape_regex(lazy_id)}\\s*:\\s*`, 'g');
87
+
88
+ return (content) => {
89
+ const next = base_hover ? base_hover(content) : content;
90
+ return next.replace(lazy_param_regex, '&');
91
+ };
92
+ }
93
+
94
+ /**
95
+ * @param {AST.Parameter[] | undefined} params
96
+ * @param {(content: string) => string} [base_hover]
97
+ * @returns {((content: string) => string) | undefined}
98
+ */
99
+ function create_function_hover_replacement(params, base_hover) {
100
+ const lazy_ids =
101
+ params
102
+ ?.filter(
103
+ (param) =>
104
+ param.type === 'Identifier' &&
105
+ param.metadata?.source_length != null &&
106
+ LAZY_PARAM_IDENTIFIER_REGEX.test(param.name),
107
+ )
108
+ .map((param) => /** @type {AST.Identifier} */ (param).name) ?? [];
109
+
110
+ if (lazy_ids.length === 0) return base_hover;
111
+
112
+ const lazy_param_regexes = lazy_ids.map(
113
+ (lazy_id) => new RegExp(`\\b${escape_regex(lazy_id)}\\s*:\\s*`, 'g'),
114
+ );
115
+
116
+ return (content) => {
117
+ let next = base_hover ? base_hover(content) : content;
118
+ for (const regex of lazy_param_regexes) {
119
+ next = next.replace(regex, '&');
120
+ }
121
+ return next;
122
+ };
123
+ }
124
+
70
125
  /**
71
126
  * @param {string} [hash]
72
127
  * @param {string} [fallback]
@@ -423,6 +478,7 @@ export function convert_source_map_to_mappings(
423
478
  generated: node.name,
424
479
  loc: node.loc,
425
480
  metadata: {},
481
+ sourceLength: node.metadata.source_length,
426
482
  };
427
483
  } else {
428
484
  token = {
@@ -430,6 +486,7 @@ export function convert_source_map_to_mappings(
430
486
  generated: node.name,
431
487
  loc: node.loc,
432
488
  metadata: {},
489
+ sourceLength: node.metadata?.source_length,
433
490
  };
434
491
  }
435
492
 
@@ -437,7 +494,36 @@ export function convert_source_map_to_mappings(
437
494
  // only if the node has a component as the parent
438
495
  token.metadata.hover = replace_label_to_component;
439
496
  }
497
+ if (node.metadata?.source_length != null && LAZY_PARAM_IDENTIFIER_REGEX.test(node.name)) {
498
+ token.metadata.hover = create_lazy_param_hover_replacement(
499
+ node.name,
500
+ node.metadata?.lazy_param_is_component ? replace_label_to_component : undefined,
501
+ );
502
+ }
503
+ if (node.metadata?.disable_verification) {
504
+ token.mappingData = { ...mapping_data, verification: false };
505
+ }
440
506
  tokens.push(token);
507
+
508
+ if (Array.isArray(node.metadata?.lazy_param_binding_mappings)) {
509
+ for (const binding_mapping of node.metadata.lazy_param_binding_mappings) {
510
+ const source_node = binding_mapping.source;
511
+ const generated_node = binding_mapping.generated;
512
+ if (!source_node?.loc || !generated_node?.loc) continue;
513
+
514
+ const mapping = get_mapping_from_node(
515
+ generated_node,
516
+ src_to_gen_map,
517
+ gen_line_offsets,
518
+ mapping_data_verify_only,
519
+ );
520
+ const source_start = /** @type {number} */ (source_node.start);
521
+ const source_end = /** @type {number} */ (source_node.end);
522
+ mapping.sourceOffsets = [source_start];
523
+ mapping.lengths = [source_end - source_start];
524
+ mappings.push(mapping);
525
+ }
526
+ }
441
527
  }
442
528
  return; // Leaf node, don't traverse further
443
529
  } else if (node.type === 'JSXIdentifier') {
@@ -767,6 +853,10 @@ export function convert_source_map_to_mappings(
767
853
  const node_fn = /** @type (typeof node) & AST.NodeWithLocation */ (node);
768
854
  const is_component = node_fn.metadata?.is_component;
769
855
  const source_func_keyword = is_component ? 'component' : 'function';
856
+ const function_hover = create_function_hover_replacement(
857
+ /** @type {AST.Parameter[]} */ (node.params),
858
+ is_component ? replace_label_to_component : undefined,
859
+ );
770
860
  let start_col = node_fn.loc.start.column;
771
861
  let start = node_fn.start;
772
862
  const async_keyword = 'async';
@@ -782,7 +872,7 @@ export function convert_source_map_to_mappings(
782
872
  mapping.lengths = [source_func_keyword.length];
783
873
  mapping.generatedOffsets = [generated_keyword_start];
784
874
  mapping.generatedLengths = ['function'.length];
785
- mapping.data.customData.hover = replace_label_to_component;
875
+ if (function_hover) mapping.data.customData.hover = function_hover;
786
876
  mappings.push(mapping);
787
877
  } else {
788
878
  if (node_fn.async) {
@@ -814,7 +904,7 @@ export function convert_source_map_to_mappings(
814
904
  column: start_col + source_func_keyword.length,
815
905
  },
816
906
  },
817
- metadata: is_component ? { hover: replace_label_to_component } : {},
907
+ metadata: function_hover ? { hover: function_hover } : {},
818
908
  });
819
909
  }
820
910
  }
@@ -826,11 +916,24 @@ export function convert_source_map_to_mappings(
826
916
  /** @type {AST.FunctionDeclaration | AST.FunctionExpression} */ (node).id &&
827
917
  !is_method
828
918
  ) {
829
- visit(
830
- /** @type {AST.Node} */ (
831
- /** @type {AST.FunctionDeclaration | AST.FunctionExpression} */ (node).id
832
- ),
919
+ const id = /** @type {AST.Identifier} */ (
920
+ /** @type {AST.FunctionDeclaration | AST.FunctionExpression} */ (node).id
921
+ );
922
+ const function_hover = create_function_hover_replacement(
923
+ /** @type {AST.Parameter[]} */ (node.params),
924
+ node.metadata?.is_component ? replace_label_to_component : undefined,
833
925
  );
926
+ if (function_hover && id.loc) {
927
+ tokens.push({
928
+ source: id.metadata?.source_name ?? id.name,
929
+ generated: id.name,
930
+ loc: id.loc,
931
+ metadata: { hover: function_hover },
932
+ sourceLength: id.metadata?.source_length,
933
+ });
934
+ } else {
935
+ visit(/** @type {AST.Node} */ (id));
936
+ }
834
937
  }
835
938
 
836
939
  if (node.typeParameters) {
@@ -839,6 +942,13 @@ export function convert_source_map_to_mappings(
839
942
 
840
943
  if (node.params) {
841
944
  for (const param of node.params) {
945
+ if (
946
+ param.type === 'Identifier' &&
947
+ param.metadata?.source_length != null &&
948
+ LAZY_PARAM_IDENTIFIER_REGEX.test(param.name)
949
+ ) {
950
+ param.metadata.lazy_param_is_component = node.metadata?.is_component === true;
951
+ }
842
952
  visit(param);
843
953
  if (param.typeAnnotation) {
844
954
  visit(param.typeAnnotation);
@@ -2067,7 +2177,7 @@ export function convert_source_map_to_mappings(
2067
2177
  token.loc.start.column,
2068
2178
  src_line_offsets,
2069
2179
  );
2070
- const source_length = source_text.length;
2180
+ const source_length = token.sourceLength ?? source_text.length;
2071
2181
  const gen_length = gen_text.length;
2072
2182
  let gen_line_col;
2073
2183
  try {
package/src/utils/ast.js CHANGED
@@ -20,6 +20,67 @@
20
20
 
21
21
  import * as b from './builders.js';
22
22
 
23
+ /**
24
+ * @param {AST.Node} node
25
+ * @returns {boolean}
26
+ */
27
+ export function is_function_node(node) {
28
+ return (
29
+ node.type === 'FunctionExpression' ||
30
+ node.type === 'ArrowFunctionExpression' ||
31
+ node.type === 'FunctionDeclaration'
32
+ );
33
+ }
34
+
35
+ /**
36
+ * @param {AST.Node} node
37
+ * @returns {boolean}
38
+ */
39
+ export function is_class_node(node) {
40
+ return node.type === 'ClassExpression' || node.type === 'ClassDeclaration';
41
+ }
42
+
43
+ /**
44
+ * @param {AST.Node} node
45
+ * @returns {boolean}
46
+ */
47
+ export function is_component_node(node) {
48
+ return node.type === 'Component';
49
+ }
50
+
51
+ /**
52
+ * Returns the closest component in an ancestry path. By default, function and
53
+ * class boundaries stop the search so callers only match direct component
54
+ * body/control-flow nodes.
55
+ *
56
+ * @param {AST.Node[]} path
57
+ * @param {boolean} [includes_functions=false]
58
+ * @returns {AST.Component | undefined}
59
+ */
60
+ export function get_component_from_path(path, includes_functions = false) {
61
+ for (let i = path.length - 1; i >= 0; i -= 1) {
62
+ const node = path[i];
63
+
64
+ if (!includes_functions && (is_function_node(node) || is_class_node(node))) {
65
+ return;
66
+ }
67
+
68
+ if (is_component_node(node)) {
69
+ return /** @type {AST.Component} */ (node);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {AST.Node[] | { path: AST.Node[] }} context_or_path
76
+ * @param {boolean} [includes_functions=false]
77
+ * @returns {AST.Component | undefined}
78
+ */
79
+ export function is_inside_component(context_or_path, includes_functions = false) {
80
+ const path = Array.isArray(context_or_path) ? context_or_path : context_or_path.path;
81
+ return get_component_from_path(path, includes_functions);
82
+ }
83
+
23
84
  /**
24
85
  * Gets the left-most identifier of a member expression or identifier.
25
86
  * @param {AST.MemberExpression | AST.Identifier} expression
package/types/index.d.ts CHANGED
@@ -37,6 +37,11 @@ export interface CompileOptions {
37
37
  dev?: boolean;
38
38
  hmr?: boolean;
39
39
  compat_kinds?: string[];
40
+ /**
41
+ * When true, non-fatal errors are collected on the result's `errors`
42
+ * array instead of being thrown. Defaults to false (strict mode: throws).
43
+ */
44
+ loose?: boolean;
40
45
  }
41
46
 
42
47
  export type NameSpace = 'html' | 'svg' | 'mathml';
@@ -45,6 +50,7 @@ interface BaseNodeMetaData {
45
50
  path: AST.Node[];
46
51
  has_template?: boolean;
47
52
  source_name?: string | '#server' | '#style';
53
+ source_length?: number;
48
54
  is_capitalized?: boolean;
49
55
  commentContainerId?: number;
50
56
  parenthesized?: boolean;
@@ -56,6 +62,12 @@ interface BaseNodeMetaData {
56
62
  lone_return?: boolean;
57
63
  forceMapping?: boolean;
58
64
  lazy_id?: string;
65
+ disable_verification?: boolean;
66
+ lazy_param_is_component?: boolean;
67
+ lazy_param_binding_mappings?: Array<{
68
+ source: AST.Identifier;
69
+ generated: AST.Identifier | AST.Literal;
70
+ }>;
59
71
  }
60
72
 
61
73
  interface FunctionMetaData extends BaseNodeMetaData {
@@ -1558,6 +1570,11 @@ export interface CompileResult {
1558
1570
  };
1559
1571
  /** The generated CSS */
1560
1572
  css: string;
1573
+ /**
1574
+ * Non-fatal errors collected during compilation. Populated only when the
1575
+ * caller passes `loose: true`; empty otherwise.
1576
+ */
1577
+ errors: CompileError[];
1561
1578
  }
1562
1579
 
1563
1580
  /**