@tsrx/core 0.1.49 → 0.1.51

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.
@@ -6,11 +6,16 @@
6
6
  */
7
7
 
8
8
  /** @import * as AST from 'estree' */
9
+ /** @import * as ESTreeJSX from 'estree-jsx' */
10
+ /** @import { Visitors } from '../../types/index' */
9
11
 
10
12
  import { walk } from 'zimmerframe';
11
13
  import * as b from '../utils/builders.js';
14
+ import { is_ast_node, is_style_element } from '../utils/ast.js';
12
15
  import { mark_class_map_selectors } from './style-ref.js';
13
16
 
17
+ export { is_style_element };
18
+
14
19
  /**
15
20
  * Mark selectors inside the stylesheet as "used" so `renderStylesheets` does
16
21
  * not comment them out.
@@ -28,36 +33,35 @@ import { mark_class_map_selectors } from './style-ref.js';
28
33
  * along with their parent: they apply where the parent's class matched, and the
29
34
  * whole rule is pruned when the parent itself is unreachable.
30
35
  *
31
- * @param {any} stylesheet
36
+ * @param {AST.CSS.StyleSheet} stylesheet
32
37
  * @param {boolean} [is_style_expression]
33
- * @returns {any}
38
+ * @returns {AST.CSS.StyleSheet}
34
39
  */
35
40
  export function prepare_stylesheet_for_render(stylesheet, is_style_expression = false) {
36
41
  if (is_style_expression) {
37
42
  mark_class_map_selectors(stylesheet);
38
43
  }
39
- walk(stylesheet, null, {
40
- _(node, { next, path }) {
41
- if (node && node.metadata && typeof node.metadata === 'object') {
42
- if (
43
- is_style_expression &&
44
- node.type === 'ComplexSelector' &&
45
- is_unreachable_via_class_map(node, path)
46
- ) {
47
- // Not in the generated class map. The analyzer pre-marks global
48
- // selectors as used, so reset, and leave the subtree untouched
49
- // no `scoped` marks that would splice the hash into pruned output.
50
- node.metadata.used = false;
51
- return;
52
- }
53
- node.metadata.used = true;
54
- if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
44
+ walk(
45
+ /** @type {AST.CSS.Node} */ (stylesheet),
46
+ null,
47
+ /** @type {Visitors<AST.CSS.Node, null>} */ ({
48
+ _(node, { next, path }) {
49
+ if (node.type === 'ComplexSelector') {
50
+ if (is_style_expression && is_unreachable_via_class_map(node, path)) {
51
+ // Not in the generated class map. The analyzer pre-marks global
52
+ // selectors as used, so reset, and leave the subtree untouched —
53
+ // no `scoped` marks that would splice the hash into pruned output.
54
+ node.metadata.used = false;
55
+ return;
56
+ }
57
+ node.metadata.used = true;
58
+ } else if (node.type === 'RelativeSelector' && !node.metadata.is_global) {
55
59
  node.metadata.scoped = true;
56
60
  }
57
- }
58
- return next();
59
- },
60
- });
61
+ return next();
62
+ },
63
+ }),
64
+ );
61
65
  return stylesheet;
62
66
  }
63
67
 
@@ -73,8 +77,8 @@ export function prepare_stylesheet_for_render(stylesheet, is_style_expression =
73
77
  * is kept because its contents render unscoped as authored and cannot be
74
78
  * pruned selector-by-selector.
75
79
  *
76
- * @param {any} complex_selector
77
- * @param {any[]} path
80
+ * @param {AST.CSS.ComplexSelector} complex_selector
81
+ * @param {AST.CSS.Node[]} path
78
82
  * @returns {boolean}
79
83
  */
80
84
  function is_unreachable_via_class_map(complex_selector, path) {
@@ -82,8 +86,8 @@ function is_unreachable_via_class_map(complex_selector, path) {
82
86
  if (complex_selector.metadata.rule?.metadata?.parent_rule != null) return false;
83
87
  if (path.some((parent) => parent.type === 'ComplexSelector')) return false;
84
88
 
85
- if (complex_selector.children?.length === 1) {
86
- const first = complex_selector.children[0]?.selectors?.[0];
89
+ if (complex_selector.children.length === 1) {
90
+ const first = complex_selector.children[0].selectors[0];
87
91
  if (first?.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) {
88
92
  return false;
89
93
  }
@@ -94,19 +98,15 @@ function is_unreachable_via_class_map(complex_selector, path) {
94
98
 
95
99
  /**
96
100
  * @param {AST.Node | null | undefined} node
97
- * @returns {node is AST.JSXStyleElement}
98
- */
99
- export function is_style_element(node) {
100
- return !!node && node.type === 'JSXStyleElement';
101
- }
102
-
103
- /**
104
- * @param {any} node
105
101
  * @returns {boolean}
106
102
  */
107
103
  export function is_composite_jsx_element(node) {
108
- const name = node?.openingElement?.name;
109
- if (node?.type !== 'JSXElement' || !name) {
104
+ if (node?.type !== 'JSXElement') {
105
+ return false;
106
+ }
107
+
108
+ const name = node.openingElement?.name;
109
+ if (!name) {
110
110
  return false;
111
111
  }
112
112
 
@@ -121,11 +121,11 @@ export function is_composite_jsx_element(node) {
121
121
  * Recursively walk native JSX nodes within a TSRX fragment and add the hash
122
122
  * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
123
123
  *
124
- * @param {any} node
124
+ * @param {AST.Node} node
125
125
  * @param {string} hash
126
126
  * @param {'class' | 'className'} [jsx_class_attr_name='class']
127
127
  * @param {boolean} [preserve_style_elements=false]
128
- * @returns {any}
128
+ * @returns {AST.Node | null}
129
129
  */
130
130
  export function annotate_with_hash(
131
131
  node,
@@ -147,20 +147,17 @@ export function annotate_with_hash(
147
147
  }
148
148
 
149
149
  if (node.type === 'JSXElement') {
150
- if (!is_composite_jsx_element(node) || node.metadata?.dynamicElement) {
151
- add_hash_class_to_jsx_element(node, hash, jsx_class_attr_name);
152
- }
153
- if (Array.isArray(node.children)) {
154
- node.children = node.children
155
- .map((/** @type {any} */ child) =>
156
- annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
157
- )
158
- .filter(Boolean);
150
+ const element = /** @type {AST.TSRXJSXElement} */ (node);
151
+ if (!is_composite_jsx_element(element) || element.metadata?.dynamicElement) {
152
+ add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name);
159
153
  }
160
- return node;
154
+ element.children = element.children
155
+ .map((child) => annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements))
156
+ .filter((child) => child !== null);
157
+ return element;
161
158
  }
162
159
 
163
- if (node.type === 'JSXStyleElement') {
160
+ if (is_style_element(node)) {
164
161
  if (preserve_style_elements) {
165
162
  node.children = [];
166
163
  return node;
@@ -168,18 +165,21 @@ export function annotate_with_hash(
168
165
  return null;
169
166
  }
170
167
 
171
- for (const key of Object.keys(node)) {
168
+ const entries = /** @type {Record<string, unknown>} */ (node);
169
+ for (const key of Object.keys(entries)) {
172
170
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
173
171
  continue;
174
172
  }
175
173
 
176
- const value = node[key];
174
+ const value = entries[key];
177
175
  if (Array.isArray(value)) {
178
- node[key] = value.map((/** @type {any} */ child) =>
179
- annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
176
+ entries[key] = value.map((child) =>
177
+ is_ast_node(child)
178
+ ? annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements)
179
+ : child,
180
180
  );
181
- } else if (value && typeof value === 'object') {
182
- node[key] = annotate_with_hash(value, hash, jsx_class_attr_name, preserve_style_elements);
181
+ } else if (is_ast_node(value)) {
182
+ entries[key] = annotate_with_hash(value, hash, jsx_class_attr_name, preserve_style_elements);
183
183
  }
184
184
  }
185
185
 
@@ -187,7 +187,8 @@ export function annotate_with_hash(
187
187
  }
188
188
 
189
189
  /**
190
- * @param {any} component
190
+ * @param {{ body: AST.Node[] }} component a node whose `body` holds the
191
+ * component's template children
191
192
  * @param {string} hash
192
193
  * @param {'class' | 'className'} [jsx_class_attr_name='class']
193
194
  * @param {boolean} [preserve_style_elements=false]
@@ -199,32 +200,53 @@ export function annotate_component_with_hash(
199
200
  jsx_class_attr_name = 'class',
200
201
  preserve_style_elements = false,
201
202
  ) {
202
- /** @type {any[]} */
203
- const body = component.body;
204
- component.body = body
205
- .filter((/** @type {any} */ child) => preserve_style_elements || !is_style_element(child))
206
- .map((/** @type {any} */ child) =>
207
- annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
208
- );
203
+ component.body = component.body
204
+ .filter((child) => preserve_style_elements || !is_style_element(child))
205
+ .map((child) => annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements))
206
+ .filter((child) => child !== null);
207
+ }
208
+
209
+ /**
210
+ * The element's `class`/`className` attribute, if it has a static-named one.
211
+ *
212
+ * @param {ESTreeJSX.JSXAttributeNode} attr
213
+ * @returns {attr is ESTreeJSX.JSXAttribute}
214
+ */
215
+ function is_class_attribute(attr) {
216
+ return (
217
+ attr.type === 'JSXAttribute' &&
218
+ attr.name.type === 'JSXIdentifier' &&
219
+ (attr.name.name === 'class' || attr.name.name === 'className')
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Merge the hash into an existing class attribute value. Returns `false` when
225
+ * the value is not a string literal, so the caller must wrap it instead.
226
+ *
227
+ * @param {ESTreeJSX.JSXAttribute['value'] | AST.Expression | ESTreeJSX.JSXEmptyExpression} value
228
+ * @param {string} hash
229
+ * @returns {boolean} whether the hash was merged into the literal in place
230
+ */
231
+ function merge_hash_into_literal(value, hash) {
232
+ if (value?.type !== 'Literal' || typeof value.value !== 'string') return false;
233
+ const merged = `${value.value} ${hash}`;
234
+ value.value = merged;
235
+ value.raw = JSON.stringify(merged);
236
+ return true;
209
237
  }
210
238
 
211
239
  /**
212
240
  * Ensure the element carries a class attribute containing the scoping hash.
213
241
  *
214
- * @param {any} element
242
+ * @param {AST.TSRXJSXElement} element
215
243
  * @param {string} hash
216
244
  * @param {'class' | 'className'} [class_attr_name='class']
217
245
  * @returns {void}
218
246
  */
219
247
  export function add_hash_class(element, hash, class_attr_name = 'class') {
220
248
  const attrs = element.openingElement.attributes;
221
- const existing = attrs.find(
222
- (/** @type {any} */ a) =>
223
- a.type === 'JSXAttribute' &&
224
- a.name &&
225
- a.name.type === 'JSXIdentifier' &&
226
- (a.name.name === 'class' || a.name.name === 'className'),
227
- );
249
+ const existing = attrs.find(is_class_attribute);
228
250
 
229
251
  if (!existing) {
230
252
  attrs.push(b.jsx_attribute(b.jsx_id(class_attr_name), b.literal(hash)));
@@ -233,17 +255,12 @@ export function add_hash_class(element, hash, class_attr_name = 'class') {
233
255
 
234
256
  const value =
235
257
  existing.value?.type === 'JSXExpressionContainer' ? existing.value.expression : existing.value;
236
- if (!value) {
237
- existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
258
+ if (!value || value.type === 'JSXEmptyExpression') {
259
+ existing.value = b.literal(hash, JSON.stringify(hash));
238
260
  return;
239
261
  }
240
262
 
241
- if (value.type === 'Literal' && typeof value.value === 'string') {
242
- const merged = `${value.value} ${hash}`;
243
- value.value = merged;
244
- value.raw = JSON.stringify(merged);
245
- return;
246
- }
263
+ if (merge_hash_into_literal(value, hash)) return;
247
264
 
248
265
  // Dynamic expression. Concatenate at runtime via template literal.
249
266
  existing.value = b.jsx_expression_container(
@@ -252,46 +269,37 @@ export function add_hash_class(element, hash, class_attr_name = 'class') {
252
269
  }
253
270
 
254
271
  /**
255
- * @param {any} element
272
+ * @param {AST.TSRXJSXElement} element
256
273
  * @param {string} hash
257
274
  * @param {'class' | 'className'} jsx_class_attr_name
258
275
  * @returns {void}
259
276
  */
260
277
  function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
261
- const attrs = element.openingElement?.attributes || (element.openingElement.attributes = []);
262
- const existing = attrs.find(
263
- (/** @type {any} */ attr) =>
264
- attr?.type === 'JSXAttribute' &&
265
- attr.name?.type === 'JSXIdentifier' &&
266
- (attr.name.name === 'class' || attr.name.name === 'className'),
267
- );
278
+ const attrs = (element.openingElement.attributes ??= []);
279
+ const existing = attrs.find(is_class_attribute);
268
280
 
269
281
  if (!existing) {
270
- const hash_literal = b.literal(hash);
271
- /** @type {any} */ (hash_literal).raw = JSON.stringify(hash);
272
- attrs.push(b.jsx_attribute(b.jsx_id(jsx_class_attr_name), hash_literal));
273
- element.attributes = attrs;
282
+ attrs.push(
283
+ b.jsx_attribute(b.jsx_id(jsx_class_attr_name), b.literal(hash, JSON.stringify(hash))),
284
+ );
274
285
  return;
275
286
  }
276
287
 
277
288
  const value = existing.value;
278
289
  if (!value) {
279
- existing.value = { type: 'Literal', value: hash, raw: JSON.stringify(hash) };
280
- element.attributes = attrs;
290
+ existing.value = b.literal(hash, JSON.stringify(hash));
281
291
  return;
282
292
  }
283
293
 
284
- if (value.type === 'Literal' && typeof value.value === 'string') {
285
- const merged = `${value.value} ${hash}`;
286
- value.value = merged;
287
- value.raw = JSON.stringify(merged);
288
- element.attributes = attrs;
294
+ if (merge_hash_into_literal(value, hash)) return;
295
+
296
+ const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
297
+ if (expression.type === 'JSXEmptyExpression') {
298
+ existing.value = b.literal(hash, JSON.stringify(hash));
289
299
  return;
290
300
  }
291
301
 
292
- const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
293
302
  existing.value = b.jsx_expression_container(
294
303
  b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]),
295
304
  );
296
- element.attributes = attrs;
297
305
  }
@@ -11,45 +11,15 @@
11
11
  PostProcessingChanges,
12
12
  LineOffsets,
13
13
  CompileError,
14
+ CssElementInfo,
15
+ CssSourceRegion,
16
+ MappingToken,
17
+ ScriptSourceRegion,
18
+ TokenClass,
14
19
  } from '../../types/index';
15
20
  @import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
16
21
  */
17
22
 
18
- /**
19
- @typedef {{
20
- start: number,
21
- end: number,
22
- content: string,
23
- id: string,
24
- }} CssSourceRegion;
25
- @typedef {{
26
- start: number,
27
- end: number,
28
- content: string,
29
- id: string,
30
- }} ScriptSourceRegion;
31
- @typedef {{
32
- source: string | null | undefined;
33
- generated: string;
34
- loc: AST.SourceLocation;
35
- metadata: PluginActionOverrides;
36
- generatedLoc?: AST.SourceLocation;
37
- end_loc?: AST.SourceLocation;
38
- sourceLength?: number;
39
- mappingData?: Partial<VolarCodeMapping['data']>;
40
- }} Token;
41
- @typedef {{
42
- name: string,
43
- line: number,
44
- column: number,
45
- offset: number,
46
- length: number,
47
- sourceOffset: number,
48
- }} TokenClass;
49
- */
50
-
51
- /** @typedef {Map<string, { scopedClasses: Map<string, { start: number; end: number; selector: any }>; hash: string } | undefined>} CssElementInfo */
52
-
53
23
  import { walk } from 'zimmerframe';
54
24
  import {
55
25
  build_src_to_gen_map,
@@ -367,7 +337,7 @@ function extract_classes(node, src_to_gen_map, gen_line_offsets, src_line_offset
367
337
  /**
368
338
  * Create Volar mappings by walking the transformed AST
369
339
  * @param {AST.Node} ast - The transformed AST
370
- * @param {AST.Node} ast_from_source - The original AST from source
340
+ * @param {AST.Program} ast_from_source - The original AST from source
371
341
  * @param {string} source - Original source code
372
342
  * @param {string} generated_code - Generated code (returned in output, not used for searching)
373
343
  * @param {RawSourceMap} source_map - Esrap source map for accurate position lookup
@@ -401,7 +371,7 @@ export function convert_source_map_to_mappings(
401
371
  errors.length > 0,
402
372
  );
403
373
 
404
- /** @type {Token[]} */
374
+ /** @type {MappingToken[]} */
405
375
  const tokens = [];
406
376
  /** @type {CssSourceRegion[]} */
407
377
  const css_regions = [];
@@ -425,7 +395,7 @@ export function convert_source_map_to_mappings(
425
395
  * _$_server_$_.foo`), esrap records multiple generated positions for the
426
396
  * same source location. Keep token mappings in generated-order by consuming
427
397
  * the next matching generated token instead of always using the first one.
428
- * @param {Token} token
398
+ * @param {MappingToken} token
429
399
  * @returns {{ line: number; column: number }}
430
400
  */
431
401
  function get_generated_position_for_token(token) {
@@ -479,16 +449,22 @@ export function convert_source_map_to_mappings(
479
449
  const mapped_comments = new Set();
480
450
 
481
451
  /**
482
- * @param {any} node
452
+ * @param {AST.Node | null | undefined} node
483
453
  * @returns {void}
484
454
  */
485
455
  function add_preserved_comment_mappings(node) {
486
- for (const key of ['leadingComments', 'trailingComments', 'innerComments', 'comments']) {
487
- const comments = node?.[key];
456
+ if (!node) return;
457
+
458
+ for (const comments of [
459
+ node.leadingComments,
460
+ node.trailingComments,
461
+ node.innerComments,
462
+ node.comments,
463
+ ]) {
488
464
  if (!Array.isArray(comments)) continue;
489
465
 
490
466
  for (const comment of comments) {
491
- if (!comment?.loc || !should_preserve_comment(comment)) continue;
467
+ if (!has_location(comment) || !should_preserve_comment(comment)) continue;
492
468
 
493
469
  const comment_key = `${comment.start}:${comment.end}`;
494
470
  if (mapped_comments.has(comment_key)) continue;
@@ -522,7 +498,7 @@ export function convert_source_map_to_mappings(
522
498
  }
523
499
 
524
500
  /**
525
- * @param {any} generated_node
501
+ * @param {AST.Identifier | ESTreeJSX.JSXIdentifier} generated_node
526
502
  * @returns {void}
527
503
  */
528
504
  function add_extra_source_mapping_tokens(generated_node) {
@@ -558,7 +534,7 @@ export function convert_source_map_to_mappings(
558
534
  // Only create mappings for identifiers with location info (from source)
559
535
  // Synthesized identifiers (created by builders) don't have .loc and are skipped
560
536
  if (node.name && node.loc) {
561
- /** @type {Token} */
537
+ /** @type {MappingToken} */
562
538
  let token;
563
539
  // Check if this identifier was changed in metadata (e.g., #Map -> RippleMap)
564
540
  // Or if it was capitalized during transformation
@@ -583,8 +559,8 @@ export function convert_source_map_to_mappings(
583
559
  if (node.metadata?.source_length != null && LAZY_PARAM_IDENTIFIER_REGEX.test(node.name)) {
584
560
  token.metadata.hover = create_lazy_param_hover_replacement(node.name);
585
561
  }
586
- if ('hover' in (node.metadata || {})) {
587
- token.metadata.hover = /** @type {any} */ (node.metadata).hover;
562
+ if (node.metadata && 'hover' in node.metadata) {
563
+ token.metadata.hover = node.metadata.hover;
588
564
  }
589
565
  if (node.metadata?.disable_verification) {
590
566
  token.mappingData = { ...mapping_data, verification: false };
@@ -624,7 +600,7 @@ export function convert_source_map_to_mappings(
624
600
  } else if (node.type === 'JSXIdentifier') {
625
601
  // JSXIdentifiers can also be capitalized (for dynamic components)
626
602
  if (node.loc && node.name) {
627
- /** @type {Token} */
603
+ /** @type {MappingToken} */
628
604
  const token = {
629
605
  source: node.metadata?.source_name ?? node.name,
630
606
  generated: node.name,
@@ -998,8 +974,7 @@ export function convert_source_map_to_mappings(
998
974
  // node-start-anchored arithmetic when tokens were not collected.
999
975
  const keyword_bound =
1000
976
  node_fn.id?.start ?? node_fn.params?.[0]?.start ?? node_fn.body?.start ?? node_fn.end;
1001
- /** @type {Array<{ value: string, start: number, end: number, loc: AST.SourceLocation }>} */
1002
- const lexer_tokens = /** @type {any} */ (ast_from_source).tsrx_keyword_tokens ?? [];
977
+ const lexer_tokens = ast_from_source.tsrx_keyword_tokens ?? [];
1003
978
  /**
1004
979
  * @param {'async' | 'function'} keyword
1005
980
  * @param {number} from