@tsrx/core 0.1.46 → 0.1.47

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": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.46",
6
+ "version": "0.1.47",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -89,6 +89,7 @@ export {
89
89
  is_function_node as isFunctionNode,
90
90
  is_function_or_class_node as isFunctionOrClassNode,
91
91
  is_function_or_component_node as isFunctionOrComponentNode,
92
+ has_location,
92
93
  is_inside_component as isInsideComponent,
93
94
  is_template_directive as isTemplateDirective,
94
95
  is_tsrx_render_output_node as isTsrxRenderOutputNode,
@@ -167,7 +168,6 @@ export {
167
168
  plan_switch_lift as planSwitchLift,
168
169
  return_value_body_to_expression as returnValueBodyToExpression,
169
170
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
170
- to_jsx_attribute as toJsxAttribute,
171
171
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
172
172
  wrap_edge_whitespace as wrapEdgeWhitespace,
173
173
  } from './transform/jsx/index.js';
@@ -189,7 +189,7 @@ export {
189
189
  } from './transform/style-ref.js';
190
190
  export {
191
191
  add_extra_source_mappings_from_matching_expression,
192
- clone_expression_node,
192
+ clone_ast_node,
193
193
  clone_identifier,
194
194
  clone_jsx_name,
195
195
  contains_component_jsx,
@@ -8,6 +8,7 @@
8
8
  import * as acorn from 'acorn';
9
9
  import { tsPlugin } from '@sveltejs/acorn-typescript';
10
10
  import { walk } from 'zimmerframe';
11
+ import { has_location } from '../utils/ast.js';
11
12
 
12
13
  /**
13
14
  * @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
@@ -607,8 +608,8 @@ export function get_comment_handlers(source, comments, index = 0) {
607
608
  }
608
609
 
609
610
  const ancestorElements = path
610
- .filter((ancestor) => isNativeTemplateNode(ancestor) && ancestor.loc)
611
- .map((ancestor) => /** @type {AST.NodeWithLocation} */ (ancestor))
611
+ .filter(has_location)
612
+ .filter(isNativeTemplateNode)
612
613
  .sort((a, b) => a.loc.start.line - b.loc.start.line);
613
614
 
614
615
  const targetAncestor = ancestorElements.find(
@@ -1,7 +1,8 @@
1
1
  /** @import * as AST from 'estree' */
2
2
  /** @import * as ESTreeJSX from 'estree-jsx' */
3
3
 
4
- import { set_location } from '../../utils/builders.js';
4
+ import * as b from '../../utils/builders.js';
5
+ import { has_location } from '../../utils/ast.js';
5
6
 
6
7
  /**
7
8
  * AST-building utilities shared across every JSX target (React, Preact,
@@ -13,15 +14,15 @@ import { set_location } from '../../utils/builders.js';
13
14
  * Attach `source_node`'s `loc` to `node` (deep), defaulting `node.metadata`
14
15
  * so downstream walks / serializers don't trip on it being undefined.
15
16
  *
16
- * @template T
17
+ * @template {AST.Node} T
17
18
  * @param {T} node
18
- * @param {any} source_node
19
+ * @param {AST.Node | AST.NodeWithLocation | undefined} source_node
19
20
  * @returns {T}
20
21
  */
21
22
  export function set_loc(node, source_node) {
22
- /** @type {any} */ (node).metadata ??= { path: [] };
23
- if (source_node?.loc) {
24
- return /** @type {T} */ (set_location(/** @type {any} */ (node), source_node, true));
23
+ node.metadata ??= { path: [] };
24
+ if (has_location(source_node)) {
25
+ return b.set_location(node, source_node, true);
25
26
  }
26
27
  return node;
27
28
  }
@@ -35,97 +36,110 @@ export function set_loc(node, source_node) {
35
36
  * @returns {AST.Identifier}
36
37
  */
37
38
  export function clone_identifier(identifier) {
38
- return set_loc(
39
- /** @type {any} */ ({
40
- type: 'Identifier',
41
- name: identifier.name,
42
- metadata: { path: [] },
43
- }),
44
- identifier,
45
- );
39
+ return set_loc(b.id(identifier.name), identifier);
46
40
  }
47
41
 
48
42
  /**
49
43
  * Clone a JSX element name (handles `JSXIdentifier`, `JSXMemberExpression`,
50
44
  * and plain `Identifier`).
51
45
  *
52
- * @param {any} name
53
- * @param {any} [source_node]
54
- * @returns {any}
46
+ * @param {ESTreeJSX.TSRXJSXOpeningElement['name']} name
47
+ * @param {AST.Node} [source_node]
48
+ * @returns {ESTreeJSX.TSRXJSXOpeningElement['name']}
55
49
  */
56
50
  export function clone_jsx_name(name, source_node = name) {
57
- if (!name) return name;
58
51
  if (name.type === 'JSXIdentifier') {
59
- return set_loc(
60
- /** @type {any} */ ({
61
- type: 'JSXIdentifier',
62
- name: name.name,
63
- metadata: name.metadata || { path: [] },
64
- }),
65
- source_node,
66
- );
52
+ return clone_jsx_identifier(name, source_node);
67
53
  }
68
54
  if (name.type === 'JSXMemberExpression') {
69
- return set_loc(
70
- /** @type {any} */ ({
71
- type: 'JSXMemberExpression',
72
- object: clone_jsx_name(name.object, source_node.object || name.object),
73
- property: clone_jsx_name(name.property, source_node.property || name.property),
74
- metadata: name.metadata || { path: [] },
75
- }),
76
- source_node,
77
- );
55
+ return clone_jsx_member_expression(name, source_node);
78
56
  }
79
57
  if (name.type === 'Identifier') {
80
- return set_loc(
81
- /** @type {any} */ ({
82
- type: 'JSXIdentifier',
83
- name: name.name,
84
- metadata: name.metadata || { path: [] },
85
- }),
86
- source_node,
87
- );
58
+ const clone = b.jsx_id(name.name);
59
+ clone.metadata = name.metadata || { path: [] };
60
+ return set_loc(clone, source_node);
88
61
  }
89
62
  return name;
90
63
  }
91
64
 
65
+ /**
66
+ * @param {ESTreeJSX.JSXIdentifier} name
67
+ * @param {AST.Node} source_node
68
+ * @returns {ESTreeJSX.JSXIdentifier}
69
+ */
70
+ function clone_jsx_identifier(name, source_node) {
71
+ const clone = b.jsx_id(name.name);
72
+ clone.metadata = name.metadata || { path: [] };
73
+ return set_loc(clone, source_node);
74
+ }
75
+
76
+ /**
77
+ * @param {ESTreeJSX.JSXMemberExpression} name
78
+ * @param {AST.Node} source_node
79
+ * @returns {ESTreeJSX.JSXMemberExpression}
80
+ */
81
+ function clone_jsx_member_expression(name, source_node) {
82
+ const member_source = source_node.type === 'JSXMemberExpression' ? source_node : name;
83
+ const object =
84
+ name.object.type === 'JSXIdentifier'
85
+ ? clone_jsx_identifier(
86
+ name.object,
87
+ member_source.object.type === 'JSXIdentifier' ? member_source.object : name.object,
88
+ )
89
+ : clone_jsx_member_expression(
90
+ name.object,
91
+ member_source.object.type === 'JSXMemberExpression' ? member_source.object : name.object,
92
+ );
93
+ const property = clone_jsx_identifier(name.property, member_source.property);
94
+ const clone = b.jsx_member(object, property);
95
+ clone.metadata = name.metadata || { path: [] };
96
+ return set_loc(clone, source_node);
97
+ }
98
+
92
99
  /**
93
100
  * Record extra source positions on a generated expression so one generated
94
101
  * range can map back to several source ranges. Used for dynamic tags, where
95
102
  * the generated `is={expr}` value stands in for both `<{expr}` and `</{expr}>`;
96
103
  * segments.js turns each recorded node into an additional mapping token.
97
- * @param {any} generated
98
- * @param {any} source
104
+ * @param {AST.Node} generated
105
+ * @param {AST.Node | null | undefined} source
99
106
  * @returns {void}
100
107
  */
101
108
  export function add_extra_source_mappings_from_matching_expression(generated, source) {
102
109
  if (!generated || !source || generated.type !== source.type) return;
103
110
 
104
111
  if (generated.type === 'Identifier' || generated.type === 'PrivateIdentifier') {
105
- if (!source.loc) return;
112
+ if (!has_location(source)) return;
106
113
  generated.metadata ??= { path: [] };
107
114
  generated.metadata.extra_source_mappings ??= [];
108
115
  generated.metadata.extra_source_mappings.push(source);
109
116
  return;
110
117
  }
111
118
 
119
+ const generated_node = /** @type {AST.TraversableAstNode} */ (generated);
120
+ const source_node = /** @type {AST.TraversableAstNode} */ (source);
112
121
  for (const key of ['expression', 'object', 'property']) {
113
- if (generated[key] && source[key]) {
114
- add_extra_source_mappings_from_matching_expression(generated[key], source[key]);
122
+ const generated_child = generated_node[key];
123
+ const source_child = source_node[key];
124
+ if (is_ast_node(generated_child) && is_ast_node(source_child)) {
125
+ add_extra_source_mappings_from_matching_expression(generated_child, source_child);
115
126
  }
116
127
  }
117
128
  }
118
129
 
130
+ /**
131
+ * @param {unknown} value
132
+ * @returns {value is AST.Node}
133
+ */
134
+ function is_ast_node(value) {
135
+ return !!value && typeof value === 'object' && 'type' in value;
136
+ }
137
+
119
138
  /**
120
139
  * @returns {AST.Literal}
121
140
  */
122
141
  export function create_null_literal() {
123
- return /** @type {any} */ ({
124
- type: 'Literal',
125
- value: null,
126
- raw: 'null',
127
- metadata: { path: [] },
128
- });
142
+ return b.literal(null, 'null');
129
143
  }
130
144
 
131
145
  /**
@@ -133,15 +147,11 @@ export function create_null_literal() {
133
147
  * @returns {AST.Identifier}
134
148
  */
135
149
  export function create_generated_identifier(name) {
136
- return /** @type {any} */ ({
137
- type: 'Identifier',
138
- name,
139
- metadata: { path: [] },
140
- });
150
+ return b.id(name);
141
151
  }
142
152
 
143
153
  /**
144
- * @param {any} node
154
+ * @param {AST.BaseNode} node
145
155
  * @param {string} message
146
156
  * @returns {Error & { pos: number, end: number }}
147
157
  */
@@ -160,33 +170,35 @@ export function create_compile_error(node, message) {
160
170
  * component hover label — without that flag those source-map adjustments
161
171
  * and editor hover features silently drop for any composite element.
162
172
  *
163
- * @param {any} id
164
- * @returns {any}
173
+ * @param {AST.Identifier | AST.MemberExpression} id
174
+ * @returns {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression}
165
175
  */
166
176
  export function identifier_to_jsx_name(id) {
167
- if (!id) return id;
168
177
  if (id.type === 'Identifier') {
169
- return set_loc(
170
- /** @type {any} */ ({
171
- type: 'JSXIdentifier',
172
- name: id.name,
173
- metadata: { ...(id.metadata || {}), path: [], is_component: /^[A-Z]/.test(id.name) },
174
- }),
175
- id,
176
- );
177
- }
178
- if (id.type === 'MemberExpression') {
179
- return set_loc(
180
- /** @type {any} */ ({
181
- type: 'JSXMemberExpression',
182
- object: identifier_to_jsx_name(id.object),
183
- property: identifier_to_jsx_name(id.property),
184
- metadata: id.metadata || { path: [] },
185
- }),
186
- id,
187
- );
178
+ return identifier_to_jsx_identifier(id);
188
179
  }
189
- return id;
180
+ const object =
181
+ id.object.type === 'Identifier'
182
+ ? identifier_to_jsx_name(id.object)
183
+ : identifier_to_jsx_name(/** @type {AST.MemberExpression} */ (id.object));
184
+ const property = identifier_to_jsx_identifier(/** @type {AST.Identifier} */ (id.property));
185
+ const name = b.jsx_member(object, property);
186
+ name.metadata = id.metadata || { path: [] };
187
+ return set_loc(name, id);
188
+ }
189
+
190
+ /**
191
+ * @param {AST.Identifier} id
192
+ * @returns {ESTreeJSX.JSXIdentifier}
193
+ */
194
+ export function identifier_to_jsx_identifier(id) {
195
+ const name = b.jsx_id(id.name);
196
+ name.metadata = {
197
+ ...(id.metadata || {}),
198
+ path: [],
199
+ is_component: /^[A-Z]/.test(id.name),
200
+ };
201
+ return set_loc(name, id);
190
202
  }
191
203
 
192
204
  /**
@@ -199,7 +211,7 @@ export function identifier_to_jsx_name(id) {
199
211
  * Used by platforms that veto static-hoisting of component JSX (Vue, Solid)
200
212
  * and by core's narrower bare-component-invocation predicate.
201
213
  *
202
- * @param {any} name
214
+ * @param {ESTreeJSX.TSRXJSXOpeningElement['name'] | AST.Identifier} name
203
215
  * @returns {boolean}
204
216
  */
205
217
  export function is_component_jsx_name(name) {
@@ -227,26 +239,25 @@ export function is_component_jsx_name(name) {
227
239
  * component instance to module identity, which doesn't help either framework
228
240
  * the way it helps React, so it's wasted output.
229
241
  *
230
- * @param {any} node
242
+ * @param {AST.Node | AST.Node[]} node
231
243
  * @returns {boolean}
232
244
  */
233
245
  export function contains_component_jsx(node) {
234
246
  if (!node || typeof node !== 'object') {
235
247
  return false;
236
248
  }
237
-
238
- if (node.type === 'JSXElement') {
249
+ if ('type' in node && node.type === 'JSXElement') {
239
250
  if (is_component_jsx_name(node.openingElement?.name)) {
240
251
  return true;
241
252
  }
242
253
  return node.children?.some(contains_component_jsx) ?? false;
243
254
  }
244
255
 
245
- if (node.type === 'JSXFragment') {
256
+ if ('type' in node && node.type === 'JSXFragment') {
246
257
  return node.children?.some(contains_component_jsx) ?? false;
247
258
  }
248
259
 
249
- if (node.type === 'JSXExpressionContainer') {
260
+ if ('type' in node && node.type === 'JSXExpressionContainer') {
250
261
  return contains_component_jsx(node.expression);
251
262
  }
252
263
 
@@ -258,7 +269,7 @@ export function contains_component_jsx(node) {
258
269
  }
259
270
 
260
271
  /**
261
- * @param {any} node
272
+ * @param {AST.Node | null | undefined} node
262
273
  * @returns {boolean}
263
274
  */
264
275
  export function is_jsx_child(node) {
@@ -287,8 +298,8 @@ export function is_jsx_child(node) {
287
298
  * the unwrapped expression is still render output rather than an executable
288
299
  * statement.
289
300
  *
290
- * @param {any} node
291
- * @returns {boolean}
301
+ * @param {AST.Node | null | undefined} node
302
+ * @returns {node is AST.Expression}
292
303
  */
293
304
  export function is_bare_render_expression(node) {
294
305
  if (!node || typeof node !== 'object') {
@@ -335,17 +346,17 @@ export function is_bare_render_expression(node) {
335
346
  * Gather the params a `for (x of y; index i)` loop should expose to its body
336
347
  * JSX (value first, optional index second).
337
348
  *
338
- * @param {any} left
339
- * @param {any} [index]
340
- * @returns {any[]}
349
+ * @param {AST.ForOfStatement['left']} left
350
+ * @param {AST.Identifier | null} [index]
351
+ * @returns {AST.Pattern[]}
341
352
  */
342
353
  export function get_for_of_iteration_params(left, index) {
343
- /** @type {any[]} */
354
+ /** @type {AST.Pattern[]} */
344
355
  const params = [];
345
356
  if (left?.type === 'VariableDeclaration' && left.declarations?.[0]) {
346
357
  params.push(left.declarations[0].id);
347
358
  } else {
348
- params.push(left);
359
+ params.push(/** @type {AST.Pattern} */ (left));
349
360
  }
350
361
  if (index) {
351
362
  params.push(index);
@@ -359,8 +370,8 @@ export function get_for_of_iteration_params(left, index) {
359
370
  * under the case. This lets `case` arms use `{ ... }` for readability
360
371
  * without the block becoming a fresh scope at the JSX level.
361
372
  *
362
- * @param {any[]} consequent
363
- * @returns {any[]}
373
+ * @param {AST.Statement[]} consequent
374
+ * @returns {AST.Statement[]}
364
375
  */
365
376
  export function flatten_switch_consequent(consequent) {
366
377
  const result = [];
@@ -404,12 +415,12 @@ function is_static_string_expression(expression) {
404
415
  * get the ternary because the AST alone can't prove they're non-null strings.
405
416
  *
406
417
  * @param {AST.Expression} expression
407
- * @param {any} [source_node]
418
+ * @param {AST.Node | AST.NodeWithLocation} [source_node]
408
419
  * @returns {AST.Expression}
409
420
  */
410
421
  export function to_text_expression(expression, source_node = expression) {
411
422
  if (is_static_string_expression(expression)) {
412
- return set_loc(clone_expression_node(expression), source_node);
423
+ return set_loc(clone_ast_node(expression), source_node);
413
424
  }
414
425
  return set_loc(
415
426
  /** @type {AST.Expression} */ ({
@@ -417,7 +428,7 @@ export function to_text_expression(expression, source_node = expression) {
417
428
  test: {
418
429
  type: 'BinaryExpression',
419
430
  operator: '==',
420
- left: clone_expression_node(expression),
431
+ left: clone_ast_node(expression),
421
432
  right: create_null_literal(),
422
433
  metadata: { path: [] },
423
434
  },
@@ -430,7 +441,7 @@ export function to_text_expression(expression, source_node = expression) {
430
441
  alternate: {
431
442
  type: 'BinaryExpression',
432
443
  operator: '+',
433
- left: clone_expression_node(expression),
444
+ left: clone_ast_node(expression),
434
445
  right: {
435
446
  type: 'Literal',
436
447
  value: '',
@@ -448,24 +459,34 @@ export function to_text_expression(expression, source_node = expression) {
448
459
  /**
449
460
  * Deep-clone an AST subtree.
450
461
  *
451
- * @param {any} node
462
+ * @template T
463
+ * @param {T} node
452
464
  * @param {boolean} with_locations
453
- * @returns {any}
465
+ * @returns {T}
454
466
  */
455
- export function clone_expression_node(node, with_locations = true) {
467
+ export function clone_ast_node(node, with_locations = true) {
456
468
  if (!node || typeof node !== 'object') return node;
457
- if (Array.isArray(node)) return node.map((child) => clone_expression_node(child, with_locations));
458
- const clone = /** @type {Record<string, any>} */ ({});
469
+ if (Array.isArray(node)) {
470
+ return /** @type {T} */ (node.map((child) => clone_ast_node(child, with_locations)));
471
+ }
472
+ const clone = { ...node };
473
+ const clone_record = /** @type {Record<string, unknown>} */ (clone);
459
474
 
460
475
  for (const key of Object.keys(node)) {
461
476
  if (!with_locations && (key === 'loc' || key === 'start' || key === 'end')) {
477
+ delete clone_record[key];
462
478
  continue;
463
479
  }
464
480
  if (key === 'metadata') {
465
- clone.metadata = node.metadata ? { ...node.metadata } : { path: [] };
481
+ const metadata = /** @type {Record<string, unknown>} */ (node).metadata;
482
+ clone_record.metadata =
483
+ metadata && typeof metadata === 'object' ? { ...metadata } : { path: [] };
466
484
  continue;
467
485
  }
468
- clone[key] = clone_expression_node(node[key], with_locations);
486
+ clone_record[key] = clone_ast_node(
487
+ /** @type {Record<string, unknown>} */ (node)[key],
488
+ with_locations,
489
+ );
469
490
  }
470
491
  return clone;
471
492
  }