@tsrx/core 0.1.4 → 0.1.7

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,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { walk } from 'zimmerframe';
9
+ import * as b from '../utils/builders.js';
9
10
 
10
11
  /**
11
12
  * Mark every selector inside the stylesheet as "used" so `renderStylesheets`
@@ -183,7 +184,7 @@ export function add_hash_class(element, hash) {
183
184
  if (!existing) {
184
185
  attrs.push({
185
186
  type: 'Attribute',
186
- name: { type: 'Identifier', name: 'class' },
187
+ name: b.id('class'),
187
188
  value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
188
189
  });
189
190
  return;
@@ -203,22 +204,7 @@ export function add_hash_class(element, hash) {
203
204
 
204
205
  // Dynamic expression. Concatenate at runtime via template literal.
205
206
  const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
206
- existing.value = {
207
- type: 'TemplateLiteral',
208
- expressions: [expression],
209
- quasis: [
210
- {
211
- type: 'TemplateElement',
212
- value: { raw: '', cooked: '' },
213
- tail: false,
214
- },
215
- {
216
- type: 'TemplateElement',
217
- value: { raw: ` ${hash}`, cooked: ` ${hash}` },
218
- tail: true,
219
- },
220
- ],
221
- };
207
+ existing.value = b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]);
222
208
  }
223
209
 
224
210
  /**
@@ -237,13 +223,9 @@ function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
237
223
  );
238
224
 
239
225
  if (!existing) {
240
- attrs.push({
241
- type: 'JSXAttribute',
242
- name: { type: 'JSXIdentifier', name: jsx_class_attr_name, metadata: { path: [] } },
243
- value: { type: 'Literal', value: hash, raw: JSON.stringify(hash) },
244
- shorthand: false,
245
- metadata: { path: [] },
246
- });
226
+ const hash_literal = b.literal(hash);
227
+ /** @type {any} */ (hash_literal).raw = JSON.stringify(hash);
228
+ attrs.push(b.jsx_attribute(b.jsx_id(jsx_class_attr_name), hash_literal));
247
229
  return;
248
230
  }
249
231
 
@@ -268,25 +250,7 @@ function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
268
250
  }
269
251
 
270
252
  const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
271
- existing.value = {
272
- type: 'JSXExpressionContainer',
273
- expression: {
274
- type: 'TemplateLiteral',
275
- expressions: [expression],
276
- quasis: [
277
- {
278
- type: 'TemplateElement',
279
- value: { raw: '', cooked: '' },
280
- tail: false,
281
- },
282
- {
283
- type: 'TemplateElement',
284
- value: { raw: ` ${hash}`, cooked: ` ${hash}` },
285
- tail: true,
286
- },
287
- ],
288
- metadata: { path: [] },
289
- },
290
- metadata: { path: [] },
291
- };
253
+ existing.value = b.jsx_expression_container(
254
+ b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]),
255
+ );
292
256
  }
@@ -10,6 +10,7 @@
10
10
  VolarMappingsResult,
11
11
  PostProcessingChanges,
12
12
  LineOffsets,
13
+ CompileError,
13
14
  } from '../../types/index';
14
15
  @import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
15
16
  */
@@ -343,6 +344,7 @@ function extract_classes(node, src_to_gen_map, gen_line_offsets, src_line_offset
343
344
  * @param {RawSourceMap} source_map - Esrap source map for accurate position lookup
344
345
  * @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
345
346
  * @param {number[]} line_offsets - Pre-computed line offsets array for generated code
347
+ * @param {CompileError[]} [errors]
346
348
  * @returns {Omit<VolarMappingsResult, 'errors'>}
347
349
  */
348
350
  export function convert_source_map_to_mappings(
@@ -353,6 +355,7 @@ export function convert_source_map_to_mappings(
353
355
  source_map,
354
356
  post_processing_changes,
355
357
  line_offsets,
358
+ errors = [],
356
359
  ) {
357
360
  /** @type {CodeMapping[]} */
358
361
  const mappings = [];
@@ -361,11 +364,12 @@ export function convert_source_map_to_mappings(
361
364
  const src_line_offsets = build_line_offsets(source);
362
365
  const gen_line_offsets = build_line_offsets(generated_code);
363
366
 
364
- const [src_to_gen_map] = build_src_to_gen_map(
367
+ const [src_to_gen_map, , source_line_generated_map] = build_src_to_gen_map(
365
368
  source_map,
366
369
  post_processing_changes,
367
370
  line_offsets,
368
371
  generated_code,
372
+ errors.length > 0,
369
373
  );
370
374
 
371
375
  /** @type {Token[]} */
@@ -2250,6 +2254,15 @@ export function convert_source_map_to_mappings(
2250
2254
  });
2251
2255
  }
2252
2256
 
2257
+ add_diagnostic_mappings(
2258
+ mappings,
2259
+ errors,
2260
+ generated_code,
2261
+ src_to_gen_map,
2262
+ source_line_generated_map,
2263
+ gen_line_offsets,
2264
+ );
2265
+
2253
2266
  // Sort mappings by start position, but prioritize narrower ranges that are fully contained
2254
2267
  // within wider ones. This ensures that specific tokens (like identifiers) take precedence
2255
2268
  // over broader ranges (like `if` consequent blocks) during language server lookups.
@@ -2351,7 +2364,7 @@ function find_component_keyword_offset(generated_code, generated_id_start) {
2351
2364
  * source: string,
2352
2365
  * generated_code: string,
2353
2366
  * source_map: RawSourceMap,
2354
- * errors?: import('../../types/index').CompileError[],
2367
+ * errors?: CompileError[],
2355
2368
  * post_processing_changes?: PostProcessingChanges,
2356
2369
  * line_offsets?: LineOffsets,
2357
2370
  * }} params
@@ -2367,20 +2380,160 @@ export function create_volar_mappings_result({
2367
2380
  post_processing_changes,
2368
2381
  line_offsets,
2369
2382
  }) {
2383
+ const result = convert_source_map_to_mappings(
2384
+ ast,
2385
+ ast_from_source,
2386
+ source,
2387
+ generated_code,
2388
+ source_map,
2389
+ /** @type {PostProcessingChanges} */ (post_processing_changes),
2390
+ line_offsets ?? build_line_offsets(generated_code),
2391
+ errors,
2392
+ );
2393
+
2370
2394
  return {
2371
- ...convert_source_map_to_mappings(
2372
- ast,
2373
- ast_from_source,
2374
- source,
2375
- generated_code,
2376
- source_map,
2377
- /** @type {PostProcessingChanges} */ (post_processing_changes),
2378
- line_offsets ?? build_line_offsets(generated_code),
2379
- ),
2395
+ ...result,
2380
2396
  errors,
2381
2397
  };
2382
2398
  }
2383
2399
 
2400
+ /**
2401
+ * Parser diagnostics can point at source-only tokens that are intentionally
2402
+ * omitted or rewritten away in generated TSX. Add a narrow mapping so the
2403
+ * language-server diagnostic plugin can translate those exact source ranges.
2404
+ *
2405
+ * @param {CodeMapping[]} mappings
2406
+ * @param {CompileError[]} errors
2407
+ * @param {string} generated_code
2408
+ * @param {Map<string, Array<{ line: number, column: number }>>} src_to_gen_map
2409
+ * @param {Map<number, Array<{ column: number, position: { line: number, column: number } }>> | null} source_line_generated_map
2410
+ * @param {LineOffsets} gen_line_offsets
2411
+ */
2412
+ function add_diagnostic_mappings(
2413
+ mappings,
2414
+ errors,
2415
+ generated_code,
2416
+ src_to_gen_map,
2417
+ source_line_generated_map,
2418
+ gen_line_offsets,
2419
+ ) {
2420
+ if (errors.length === 0 || !source_line_generated_map) {
2421
+ return;
2422
+ }
2423
+
2424
+ /** @type {CodeMapping[]} */
2425
+ const diagnostic_mappings = [];
2426
+
2427
+ for (const error of errors) {
2428
+ const start = error.pos;
2429
+ if (start === undefined) continue;
2430
+ if (has_exact_source_map_position(error, src_to_gen_map)) continue;
2431
+
2432
+ const end = error.end && error.end > start ? error.end : start + 1;
2433
+ const length = end - start;
2434
+ const generated_start = get_nearest_generated_offset_from_source_line_map(
2435
+ error,
2436
+ source_line_generated_map,
2437
+ gen_line_offsets,
2438
+ generated_code,
2439
+ );
2440
+ if (generated_start === null) continue;
2441
+
2442
+ diagnostic_mappings.push({
2443
+ sourceOffsets: [start],
2444
+ generatedOffsets: [generated_start],
2445
+ lengths: [length],
2446
+ generatedLengths: [
2447
+ generated_code.length === 0
2448
+ ? 0
2449
+ : Math.max(1, Math.min(length, generated_code.length - generated_start)),
2450
+ ],
2451
+ data: {
2452
+ ...mapping_data_verify_only,
2453
+ customData: {},
2454
+ },
2455
+ });
2456
+ }
2457
+
2458
+ mappings.unshift(...diagnostic_mappings);
2459
+ }
2460
+
2461
+ /**
2462
+ * @param {CompileError} error
2463
+ * @param {Map<string, Array<{ line: number, column: number }>>} src_to_gen_map
2464
+ */
2465
+ function has_exact_source_map_position(error, src_to_gen_map) {
2466
+ const loc = error.loc?.start;
2467
+ return !!loc && src_to_gen_map.has(`${loc.line}:${loc.column}`);
2468
+ }
2469
+
2470
+ /**
2471
+ * @param {CompileError} error
2472
+ * @param {Map<number, Array<{ column: number, position: { line: number, column: number } }>>} source_line_generated_map
2473
+ * @param {LineOffsets} gen_line_offsets
2474
+ * @param {string} generated_code
2475
+ */
2476
+ function get_nearest_generated_offset_from_source_line_map(
2477
+ error,
2478
+ source_line_generated_map,
2479
+ gen_line_offsets,
2480
+ generated_code,
2481
+ ) {
2482
+ const loc = error.loc?.start;
2483
+ if (!loc || generated_code.length === 0) {
2484
+ return null;
2485
+ }
2486
+
2487
+ const position = get_nearest_source_line_generated_position(
2488
+ source_line_generated_map,
2489
+ loc.line,
2490
+ loc.column,
2491
+ );
2492
+ if (!position) {
2493
+ return null;
2494
+ }
2495
+
2496
+ const generated_offset =
2497
+ loc_to_offset(position.line, position.column, gen_line_offsets) +
2498
+ ('sourceColumn' in position ? loc.column - position.sourceColumn : 0);
2499
+ return Math.max(0, Math.min(generated_offset, generated_code.length - 1));
2500
+ }
2501
+
2502
+ /**
2503
+ * @param {Map<number, Array<{ column: number, position: { line: number, column: number } }>>} source_line_generated_map
2504
+ * @param {number} line
2505
+ * @param {number} column
2506
+ * @returns {{ line: number, column: number, sourceColumn: number } | null}
2507
+ */
2508
+ function get_nearest_source_line_generated_position(source_line_generated_map, line, column) {
2509
+ const line_positions = source_line_generated_map.get(line);
2510
+ if (!line_positions?.length) {
2511
+ return null;
2512
+ }
2513
+ line_positions.sort((a, b) => a.column - b.column);
2514
+
2515
+ let low = 0;
2516
+ let high = line_positions.length - 1;
2517
+ let best = -1;
2518
+ while (low <= high) {
2519
+ const mid = (low + high) >> 1;
2520
+ if (line_positions[mid].column <= column) {
2521
+ best = mid;
2522
+ low = mid + 1;
2523
+ } else {
2524
+ high = mid - 1;
2525
+ }
2526
+ }
2527
+
2528
+ if (best === -1) {
2529
+ return null;
2530
+ }
2531
+
2532
+ const entry = line_positions[best];
2533
+ const position = entry.position;
2534
+ return { line: position.line, column: position.column, sourceColumn: entry.column };
2535
+ }
2536
+
2384
2537
  /**
2385
2538
  * Remove byte-for-byte duplicate mappings. Framework compilers that extract
2386
2539
  * shared helpers or replay JSX can emit identical mapping entries for the
package/src/utils/ast.js CHANGED
@@ -32,6 +32,19 @@ export function is_function_node(node) {
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * @param {AST.Node} node
37
+ * @returns {boolean}
38
+ */
39
+ export function is_function_or_component_node(node) {
40
+ return (
41
+ node.type === 'FunctionDeclaration' ||
42
+ node.type === 'FunctionExpression' ||
43
+ node.type === 'ArrowFunctionExpression' ||
44
+ node.type === 'Component'
45
+ );
46
+ }
47
+
35
48
  /**
36
49
  * @param {AST.Node} node
37
50
  * @returns {boolean}
@@ -57,10 +57,12 @@ export function assignment_pattern(left, right) {
57
57
  /**
58
58
  * @param {Array<AST.Pattern>} params
59
59
  * @param {AST.BlockStatement | AST.Expression} body
60
+ * @param {boolean} [async]
61
+ * @param {AST.TSTypeParameterDeclaration} [type_parameters]
60
62
  * @param {AST.NodeWithLocation} [loc_info]
61
63
  * @returns {AST.ArrowFunctionExpression}
62
64
  */
63
- export function arrow(params, body, async = false, loc_info) {
65
+ export function arrow(params, body, async = false, type_parameters, loc_info) {
64
66
  const node = /** @type {AST.ArrowFunctionExpression} */ ({
65
67
  type: 'ArrowFunctionExpression',
66
68
  params,
@@ -68,6 +70,7 @@ export function arrow(params, body, async = false, loc_info) {
68
70
  expression: body.type !== 'BlockStatement',
69
71
  generator: false,
70
72
  async,
73
+ typeParameters: type_parameters,
71
74
  metadata: { path: [] },
72
75
  });
73
76
 
@@ -296,12 +299,16 @@ export function export_builder(
296
299
  * @param {AST.Identifier} id
297
300
  * @param {AST.Pattern[]} params
298
301
  * @param {AST.BlockStatement} body
302
+ * @param {boolean} [async]
303
+ * @param {AST.TSTypeParameterDeclaration} [type_parameters]
304
+ * @param
299
305
  * @returns {AST.FunctionDeclaration}
300
306
  */
301
- export function function_declaration(id, params, body, async = false) {
307
+ export function function_declaration(id, params, body, async = false, type_parameters) {
302
308
  return {
303
309
  type: 'FunctionDeclaration',
304
310
  id,
311
+ typeParameters: type_parameters,
305
312
  params,
306
313
  body,
307
314
  generator: false,
@@ -370,11 +377,12 @@ export function init(name, value) {
370
377
 
371
378
  /**
372
379
  * @param {boolean | string | number | bigint | false | RegExp | null | undefined} value
380
+ * @param {string} [raw]
373
381
  * @param {AST.NodeWithLocation} [loc_info]
374
382
  * @returns {AST.Literal}
375
383
  */
376
- export function literal(value, loc_info) {
377
- const node = /** @type {AST.Literal} */ ({ type: 'Literal', value, metadata: { path: [] } });
384
+ export function literal(value, raw, loc_info) {
385
+ const node = /** @type {AST.Literal} */ ({ type: 'Literal', value, raw, metadata: { path: [] } });
378
386
 
379
387
  return set_location(node, loc_info);
380
388
  }
@@ -940,16 +948,18 @@ export function method(kind, key, params, body, computed = false, is_static = fa
940
948
  * @param {AST.Pattern[]} params
941
949
  * @param {AST.BlockStatement} body
942
950
  * @param {boolean} async
951
+ * @param {AST.TSTypeParameterDeclaration} [type_parameters]
943
952
  * @param {AST.NodeWithLocation} [loc_info]
944
953
  * @returns {AST.FunctionExpression}
945
954
  */
946
- function function_builder(id, params, body, async = false, loc_info) {
955
+ function function_builder(id, params, body, async = false, type_parameters, loc_info) {
947
956
  /** @type {AST.FunctionExpression} */
948
957
  const node = {
949
958
  type: 'FunctionExpression',
950
959
  id,
951
960
  params,
952
961
  body,
962
+ typeParameters: type_parameters,
953
963
  generator: false,
954
964
  async,
955
965
  metadata: { path: [] },
@@ -990,7 +1000,7 @@ export function import_all(as, source, attributes = [], importKind = 'value') {
990
1000
  }
991
1001
 
992
1002
  /**
993
- * @param {Array<[string, string, AST.ImportDeclaration['importKind']]>} parts
1003
+ * @param {Array<[string, string] | [string, string, AST.ImportDeclaration['importKind']]>} parts
994
1004
  * @param {string} source
995
1005
  * @param {Array<AST.ImportAttribute>} attributes
996
1006
  * @param {AST.ImportDeclaration['importKind']} importKind
@@ -1001,13 +1011,41 @@ export function imports(parts, source, attributes = [], importKind = 'value') {
1001
1011
  type: 'ImportDeclaration',
1002
1012
  source: literal(source),
1003
1013
  attributes,
1004
- specifiers: parts.map((p) => ({
1005
- type: 'ImportSpecifier',
1006
- imported: id(p[0]),
1007
- local: id(p[1]),
1008
- importKind: p.length > 2 ? p[2] : 'value',
1009
- metadata: { path: [] },
1010
- })),
1014
+ specifiers: parts.map((p) => import_specifier(p[0], p[1], p.length > 2 ? p[2] : 'value')),
1015
+ importKind,
1016
+ metadata: { path: [] },
1017
+ };
1018
+ }
1019
+
1020
+ /**
1021
+ * @param {string | AST.Identifier} imported
1022
+ * @param {string | AST.Identifier} [local]
1023
+ * @param {AST.ImportDeclaration['importKind']} [importKind]
1024
+ * @returns {AST.ImportSpecifier}
1025
+ */
1026
+ export function import_specifier(imported, local = imported, importKind = 'value') {
1027
+ return {
1028
+ type: 'ImportSpecifier',
1029
+ imported: typeof imported === 'string' ? id(imported) : imported,
1030
+ local: typeof local === 'string' ? id(local) : local,
1031
+ importKind,
1032
+ metadata: { path: [] },
1033
+ };
1034
+ }
1035
+
1036
+ /**
1037
+ * @param {Array<AST.ImportSpecifier | AST.ImportDefaultSpecifier | AST.ImportNamespaceSpecifier>} specifiers
1038
+ * @param {string} source
1039
+ * @param {Array<AST.ImportAttribute>} [attributes]
1040
+ * @param {AST.ImportDeclaration['importKind']} [importKind]
1041
+ * @returns {AST.ImportDeclaration}
1042
+ */
1043
+ export function import_declaration(specifiers, source, attributes = [], importKind = 'value') {
1044
+ return {
1045
+ type: 'ImportDeclaration',
1046
+ source: literal(source),
1047
+ specifiers,
1048
+ attributes,
1011
1049
  importKind,
1012
1050
  metadata: { path: [] },
1013
1051
  };
@@ -0,0 +1,158 @@
1
+ const VOID_ELEMENT_NAMES = [
2
+ 'area',
3
+ 'base',
4
+ 'br',
5
+ 'col',
6
+ 'command',
7
+ 'embed',
8
+ 'hr',
9
+ 'img',
10
+ 'input',
11
+ 'keygen',
12
+ 'link',
13
+ 'meta',
14
+ 'param',
15
+ 'source',
16
+ 'track',
17
+ 'wbr',
18
+ ];
19
+
20
+ /**
21
+ * Returns true if name is a void element
22
+ * @param {string} name
23
+ * @returns {boolean}
24
+ */
25
+ export function is_void_element(name) {
26
+ return VOID_ELEMENT_NAMES.includes(name) || name.toLowerCase() === '!doctype';
27
+ }
28
+
29
+ const RESERVED_WORDS = [
30
+ 'arguments',
31
+ 'await',
32
+ 'break',
33
+ 'case',
34
+ 'catch',
35
+ 'class',
36
+ 'const',
37
+ 'continue',
38
+ 'debugger',
39
+ 'default',
40
+ 'delete',
41
+ 'do',
42
+ 'else',
43
+ 'enum',
44
+ 'eval',
45
+ 'export',
46
+ 'extends',
47
+ 'false',
48
+ 'finally',
49
+ 'for',
50
+ 'function',
51
+ 'if',
52
+ 'implements',
53
+ 'import',
54
+ 'in',
55
+ 'instanceof',
56
+ 'interface',
57
+ 'let',
58
+ 'new',
59
+ 'null',
60
+ 'package',
61
+ 'private',
62
+ 'protected',
63
+ 'public',
64
+ 'return',
65
+ 'static',
66
+ 'super',
67
+ 'switch',
68
+ 'this',
69
+ 'throw',
70
+ 'true',
71
+ 'try',
72
+ 'typeof',
73
+ 'var',
74
+ 'void',
75
+ 'while',
76
+ 'with',
77
+ 'yield',
78
+ ];
79
+
80
+ /**
81
+ * Returns true if word is a reserved JS keyword
82
+ * @param {string} word
83
+ * @returns {boolean}
84
+ */
85
+ export function is_reserved(word) {
86
+ return RESERVED_WORDS.includes(word);
87
+ }
88
+
89
+ /**
90
+ * Attributes that are boolean, i.e. they are present or not present.
91
+ */
92
+ const DOM_BOOLEAN_ATTRIBUTES = [
93
+ 'allowfullscreen',
94
+ 'async',
95
+ 'autofocus',
96
+ 'autoplay',
97
+ 'checked',
98
+ 'controls',
99
+ 'default',
100
+ 'disabled',
101
+ 'formnovalidate',
102
+ 'hidden',
103
+ 'indeterminate',
104
+ 'inert',
105
+ 'ismap',
106
+ 'loop',
107
+ 'multiple',
108
+ 'muted',
109
+ 'nomodule',
110
+ 'novalidate',
111
+ 'open',
112
+ 'playsinline',
113
+ 'readonly',
114
+ 'required',
115
+ 'reversed',
116
+ 'seamless',
117
+ 'selected',
118
+ 'webkitdirectory',
119
+ 'defer',
120
+ 'disablepictureinpicture',
121
+ 'disableremoteplayback',
122
+ ];
123
+
124
+ /**
125
+ * Returns true if name is a boolean DOM attribute
126
+ * @param {string} name
127
+ * @returns {boolean}
128
+ */
129
+ export function is_boolean_attribute(name) {
130
+ return DOM_BOOLEAN_ATTRIBUTES.includes(name);
131
+ }
132
+
133
+ const DOM_PROPERTIES = [
134
+ ...DOM_BOOLEAN_ATTRIBUTES,
135
+ 'formNoValidate',
136
+ 'isMap',
137
+ 'noModule',
138
+ 'playsInline',
139
+ 'readOnly',
140
+ 'value',
141
+ 'volume',
142
+ 'defaultValue',
143
+ 'defaultChecked',
144
+ 'srcObject',
145
+ 'noValidate',
146
+ 'allowFullscreen',
147
+ 'disablePictureInPicture',
148
+ 'disableRemotePlayback',
149
+ ];
150
+
151
+ /**
152
+ * Returns true if name is a DOM property
153
+ * @param {string} name
154
+ * @returns {boolean}
155
+ */
156
+ export function is_dom_property(name) {
157
+ return DOM_PROPERTIES.includes(name);
158
+ }