@tsrx/core 0.1.20 → 0.1.24

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.
@@ -38,32 +38,6 @@ export function DestructuringErrors() {
38
38
  return this;
39
39
  }
40
40
 
41
- /**
42
- * Convert JSX node types to regular JavaScript node types
43
- * @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
44
- * @returns {AST.Identifier | AST.MemberExpression | AST.Node} The converted node
45
- */
46
- export function convert_from_jsx(node) {
47
- /** @type {AST.Identifier | AST.MemberExpression | AST.Node} */
48
- let converted_node;
49
- if (node.type === 'JSXIdentifier') {
50
- converted_node = /** @type {AST.Identifier} */ (/** @type {unknown} */ (node));
51
- converted_node.type = 'Identifier';
52
- } else if (node.type === 'JSXMemberExpression') {
53
- converted_node = /** @type {AST.MemberExpression} */ (/** @type {unknown} */ (node));
54
- converted_node.type = 'MemberExpression';
55
- converted_node.object = /** @type {AST.Identifier | AST.MemberExpression} */ (
56
- convert_from_jsx(converted_node.object)
57
- );
58
- converted_node.property = /** @type {AST.Identifier} */ (
59
- convert_from_jsx(converted_node.property)
60
- );
61
- } else {
62
- converted_node = node;
63
- }
64
- return converted_node;
65
- }
66
-
67
41
  const regex_whitespace_only = /\s/;
68
42
 
69
43
  /**
@@ -95,18 +69,18 @@ export function skipWhitespace(parser) {
95
69
  }
96
70
 
97
71
  /**
98
- * @param {AST.Node | null | undefined} node
72
+ * @param {AST.Node | ESTreeJSX.JSXText | null | undefined} node
99
73
  * @returns {boolean}
100
74
  */
101
75
  export function isWhitespaceTextNode(node) {
102
- if (!node || node.type !== 'Text') {
76
+ if (!node) {
103
77
  return false;
104
78
  }
105
79
 
106
- const expr = node.expression;
107
- if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
108
- return /^\s*$/.test(expr.value);
80
+ if (node.type === 'JSXText') {
81
+ return /^\s*$/.test(node.value);
109
82
  }
83
+
110
84
  return false;
111
85
  }
112
86
 
@@ -298,6 +272,87 @@ export function get_comment_handlers(source, comments, index = 0) {
298
272
  return null;
299
273
  }
300
274
 
275
+ /**
276
+ * @param {any} node
277
+ * @returns {node is (ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment) & AST.NodeWithLocation}
278
+ */
279
+ function isNativeTemplateNode(node) {
280
+ return (
281
+ (node?.type === 'JSXElement' ||
282
+ node?.type === 'JSXFragment' ||
283
+ node?.type === 'JSXStyleElement') &&
284
+ node.metadata?.native_tsrx
285
+ );
286
+ }
287
+
288
+ /**
289
+ * @param {any} node
290
+ * @returns {node is (ESTreeJSX.JSXElement | AST.JSXStyleElement) & AST.NodeWithLocation}
291
+ */
292
+ function isNativeTemplateElement(node) {
293
+ return (
294
+ (node?.type === 'JSXElement' || node?.type === 'JSXStyleElement') &&
295
+ node.metadata?.native_tsrx
296
+ );
297
+ }
298
+
299
+ /**
300
+ * @param {any} node
301
+ * @returns {AST.Node[]}
302
+ */
303
+ function getTemplateChildren(node) {
304
+ return Array.isArray(node?.children)
305
+ ? /** @type {AST.Node[]} */ (/** @type {unknown} */ (node.children))
306
+ : [];
307
+ }
308
+
309
+ /**
310
+ * @param {any} node
311
+ * @returns {node is (ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment) & AST.NodeWithLocation}
312
+ */
313
+ function isEmptyTemplateNode(node) {
314
+ return isNativeTemplateNode(node) && getTemplateChildren(node).length === 0;
315
+ }
316
+
317
+ /**
318
+ * @param {any} node
319
+ * @returns {any}
320
+ */
321
+ function getNodeMetadata(node) {
322
+ const target = /** @type {AST.Node} */ (/** @type {unknown} */ (node));
323
+ target.metadata ??= { path: [] };
324
+ return target.metadata;
325
+ }
326
+
327
+ /**
328
+ * @param {any} node
329
+ * @param {AST.CommentWithLocation} comment
330
+ */
331
+ function pushInnerComment(node, comment) {
332
+ const target = /** @type {any} */ (node);
333
+ (target.innerComments ||= []).push(comment);
334
+ }
335
+
336
+ /**
337
+ * @param {any} node
338
+ * @returns {boolean}
339
+ */
340
+ function hasInnerComments(node) {
341
+ return !!(/** @type {any} */ (node).innerComments?.length);
342
+ }
343
+
344
+ /**
345
+ * @param {ESTreeJSX.JSXElement | AST.JSXStyleElement} node
346
+ * @returns {string | null}
347
+ */
348
+ function getJSXElementName(node) {
349
+ const name = node.openingElement?.name;
350
+ if (!name) return null;
351
+ if (name.type === 'JSXIdentifier') return name.name;
352
+ if (name.type === 'JSXNamespacedName') return `${name.namespace.name}:${name.name.name}`;
353
+ return null;
354
+ }
355
+
301
356
  return {
302
357
  /**
303
358
  * @type {Parse.Options['onComment']}
@@ -348,19 +403,13 @@ export function get_comment_handlers(source, comments, index = 0) {
348
403
  _(node, { next, path }) {
349
404
  const metadata = /** @type {AST.Node} */ (node)?.metadata;
350
405
 
351
- /**
352
- * Check if a comment is inside an attribute expression
353
- * of any ancestor Elements.
354
- * @returns {boolean}
355
- */
406
+ /** @returns {boolean} */
356
407
  function isCommentInsideAttributeExpression() {
357
408
  for (let i = path.length - 1; i >= 0; i--) {
358
409
  const ancestor = path[i];
359
410
  if (
360
411
  ancestor &&
361
- (ancestor.type === 'JSXAttribute' ||
362
- ancestor.type === 'Attribute' ||
363
- ancestor.type === 'JSXExpressionContainer')
412
+ (ancestor.type === 'JSXAttribute' || ancestor.type === 'JSXExpressionContainer')
364
413
  ) {
365
414
  return true;
366
415
  }
@@ -369,8 +418,6 @@ export function get_comment_handlers(source, comments, index = 0) {
369
418
  }
370
419
 
371
420
  /**
372
- * Check if a comment is inside any attribute of ancestor Elements,
373
- * but NOT if we're currently traversing inside that attribute.
374
421
  * @param {AST.CommentWithLocation} comment
375
422
  * @returns {boolean}
376
423
  */
@@ -378,14 +425,17 @@ export function get_comment_handlers(source, comments, index = 0) {
378
425
  for (let i = path.length - 1; i >= 0; i--) {
379
426
  const ancestor = path[i];
380
427
  // we would definitely reach the attribute first before getting to the element
381
- if (ancestor.type === 'JSXAttribute' || ancestor.type === 'Attribute') {
428
+ if (ancestor.type === 'JSXAttribute') {
382
429
  return false;
383
430
  }
384
- if (ancestor && ancestor.type === 'Element') {
385
- for (const attr of /** @type {(AST.Attribute & AST.NodeWithLocation)[]} */ (
386
- ancestor.attributes
387
- )) {
388
- if (comment.start >= attr.start && comment.end <= attr.end) {
431
+ if (isNativeTemplateElement(ancestor)) {
432
+ for (const attr of ancestor.openingElement.attributes) {
433
+ if (
434
+ attr.start !== undefined &&
435
+ attr.end !== undefined &&
436
+ comment.start >= attr.start &&
437
+ comment.end <= attr.end
438
+ ) {
389
439
  return true;
390
440
  }
391
441
  }
@@ -395,23 +445,20 @@ export function get_comment_handlers(source, comments, index = 0) {
395
445
  }
396
446
 
397
447
  /**
398
- * If a comment is located between an empty Element's opening and closing tags,
399
- * attach it to the Element as `innerComments`.
400
448
  * @param {AST.CommentWithLocation} comment
401
- * @returns {AST.Element | null}
449
+ * @returns {((ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment) & AST.NodeWithLocation) | null}
402
450
  */
403
451
  function getEmptyElementInnerCommentTarget(comment) {
404
- const element = /** @type {AST.Element | undefined} */ (
405
- path.findLast((ancestor) => ancestor && ancestor.type === 'Element')
406
- );
452
+ const element = path.findLast((ancestor) => isNativeTemplateNode(ancestor));
453
+ const openingEnd =
454
+ element?.type === 'JSXFragment'
455
+ ? element.openingFragment?.end
456
+ : element?.openingElement?.end;
407
457
  if (
408
458
  !element ||
409
- element.children.length > 0 ||
410
- !element.closingElement ||
411
- !(
412
- comment.start >= /** @type {AST.NodeWithLocation} */ (element.openingElement).end &&
413
- comment.end <= /** @type {AST.NodeWithLocation} */ (element).end
414
- )
459
+ !isEmptyTemplateNode(element) ||
460
+ openingEnd === undefined ||
461
+ !(comment.start >= openingEnd && comment.end <= element.end)
415
462
  ) {
416
463
  return null;
417
464
  }
@@ -426,22 +473,16 @@ export function get_comment_handlers(source, comments, index = 0) {
426
473
  // parent <style> element's content range so they don't leak to
427
474
  // subsequent JS nodes.
428
475
  if (node.type === 'StyleSheet') {
429
- const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
430
- path.findLast(
431
- (ancestor) =>
432
- ancestor &&
433
- ancestor.type === 'Element' &&
434
- ancestor.id &&
435
- /** @type {AST.Identifier} */ (ancestor.id).name === 'style',
436
- )
437
- );
476
+ const styleElement =
477
+ /** @type {(ESTreeJSX.JSXElement & AST.NodeWithLocation) | undefined} */ (
478
+ path.findLast(
479
+ (ancestor) =>
480
+ isNativeTemplateElement(ancestor) && getJSXElementName(ancestor) === 'style',
481
+ )
482
+ );
438
483
  if (styleElement) {
439
- const cssStart =
440
- /** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
441
- styleElement.start;
442
- const cssEnd =
443
- /** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
444
- styleElement.end;
484
+ const cssStart = styleElement.openingElement?.end ?? styleElement.start;
485
+ const cssEnd = styleElement.closingElement?.start ?? styleElement.end;
445
486
  while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
446
487
  comments.shift();
447
488
  }
@@ -453,8 +494,7 @@ export function get_comment_handlers(source, comments, index = 0) {
453
494
  // For empty template elements, keep comments as `innerComments`.
454
495
  // The Prettier plugin uses `innerComments` to preserve them and
455
496
  // to avoid collapsing the element into self-closing syntax.
456
- const isEmptyElement =
457
- node.type === 'Element' && (!node.children || node.children.length === 0);
497
+ const isEmptyElement = isEmptyTemplateNode(node);
458
498
  if (!isEmptyElement) {
459
499
  while (
460
500
  comments[0] &&
@@ -468,9 +508,7 @@ export function get_comment_handlers(source, comments, index = 0) {
468
508
  // before the child element is pushed to the parser's #path, causing
469
509
  // comments inside the child to get the parent's containerId.
470
510
  const commentStart = comments[0].start;
471
- const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
472
- node
473
- ).children?.some(
511
+ const isInsideChildElement = getTemplateChildren(node).some(
474
512
  (child) =>
475
513
  child &&
476
514
  child.start !== undefined &&
@@ -491,7 +529,7 @@ export function get_comment_handlers(source, comments, index = 0) {
491
529
  comments[0] &&
492
530
  comments[0].start < /** @type {AST.NodeWithLocation} */ (node).start
493
531
  ) {
494
- // Skip comments that are inside an attribute of an ancestor Element.
532
+ // Skip comments that are inside an attribute of an ancestor JSX element.
495
533
  // Since zimmerframe visits children before attributes, we need to leave
496
534
  // these comments for when the attribute nodes are visited.
497
535
  if (
@@ -506,7 +544,8 @@ export function get_comment_handlers(source, comments, index = 0) {
506
544
  /** @type {AST.CommentWithLocation} */ (comments[0]),
507
545
  );
508
546
  if (maybeInner) {
509
- (maybeInner.innerComments ||= []).push(
547
+ pushInnerComment(
548
+ maybeInner,
510
549
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
511
550
  );
512
551
  continue;
@@ -536,17 +575,18 @@ export function get_comment_handlers(source, comments, index = 0) {
536
575
  continue;
537
576
  }
538
577
 
539
- const ancestorElements = /** @type {(AST.Element & AST.NodeWithLocation)[]} */ (
540
- path.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
541
- ).sort((a, b) => a.loc.start.line - b.loc.start.line);
578
+ const ancestorElements = path
579
+ .filter((ancestor) => isNativeTemplateNode(ancestor) && ancestor.loc)
580
+ .map((ancestor) => /** @type {AST.NodeWithLocation} */ (ancestor))
581
+ .sort((a, b) => a.loc.start.line - b.loc.start.line);
542
582
 
543
583
  const targetAncestor = ancestorElements.find(
544
584
  (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
545
585
  );
546
586
 
547
587
  if (targetAncestor) {
548
- targetAncestor.metadata ??= { path: [] };
549
- (targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
588
+ const targetMetadata = getNodeMetadata(targetAncestor);
589
+ (targetMetadata.elementLeadingComments ||= []).push(comment);
550
590
  continue;
551
591
  }
552
592
 
@@ -595,8 +635,8 @@ export function get_comment_handlers(source, comments, index = 0) {
595
635
  return;
596
636
  }
597
637
  }
598
- // Handle empty Element nodes the same way as empty BlockStatements
599
- if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
638
+ // Handle empty template nodes the same way as empty BlockStatements
639
+ if (isEmptyTemplateNode(node)) {
600
640
  // Collect all comments that fall within this empty element
601
641
  while (
602
642
  comments[0] &&
@@ -604,9 +644,26 @@ export function get_comment_handlers(source, comments, index = 0) {
604
644
  comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
605
645
  ) {
606
646
  const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
607
- (node.innerComments ||= []).push(comment);
647
+ pushInnerComment(node, comment);
608
648
  }
609
- if (node.innerComments && node.innerComments.length > 0) {
649
+ if (hasInnerComments(node)) {
650
+ return;
651
+ }
652
+ }
653
+
654
+ // Trailing comments after the last statement/render inside a `@{ … }`
655
+ // code block (before its `}`) have no following node to attach to and
656
+ // would otherwise be claimed by the enclosing element's closing tag.
657
+ // Claim them here as the block's inner comments.
658
+ if (node.type === 'JSXCodeBlock') {
659
+ while (
660
+ comments[0] &&
661
+ comments[0].start >= /** @type {AST.NodeWithLocation} */ (node).start &&
662
+ comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end
663
+ ) {
664
+ pushInnerComment(node, /** @type {AST.CommentWithLocation} */ (comments.shift()));
665
+ }
666
+ if (comments.length === 0) {
610
667
  return;
611
668
  }
612
669
  }
@@ -682,7 +739,8 @@ export function get_comment_handlers(source, comments, index = 0) {
682
739
 
683
740
  const maybeInner = getEmptyElementInnerCommentTarget(potentialComment);
684
741
  if (maybeInner) {
685
- (maybeInner.innerComments ||= []).push(
742
+ pushInnerComment(
743
+ maybeInner,
686
744
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
687
745
  );
688
746
  continue;
@@ -712,7 +770,8 @@ export function get_comment_handlers(source, comments, index = 0) {
712
770
 
713
771
  const maybeInner = getEmptyElementInnerCommentTarget(comment);
714
772
  if (maybeInner) {
715
- (maybeInner.innerComments ||= []).push(
773
+ pushInnerComment(
774
+ maybeInner,
716
775
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
717
776
  );
718
777
  continue;
@@ -727,7 +786,8 @@ export function get_comment_handlers(source, comments, index = 0) {
727
786
  /** @type {AST.CommentWithLocation} */ (comments[0]),
728
787
  );
729
788
  if (maybeInner) {
730
- (maybeInner.innerComments ||= []).push(
789
+ pushInnerComment(
790
+ maybeInner,
731
791
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
732
792
  );
733
793
  return;
@@ -814,7 +874,7 @@ export function get_comment_handlers(source, comments, index = 0) {
814
874
  // check if there's also a blank line after the comment(s) before the next node
815
875
  // If so, attach comments as trailing to preserve the grouping
816
876
  // Only do this for statement-level contexts (BlockStatement, Program),
817
- // not for Element children or other contexts
877
+ // not for JSX element children or other contexts
818
878
  const isStatementContext =
819
879
  parent.type === 'BlockStatement' || parent.type === 'Program';
820
880
 
@@ -850,16 +910,14 @@ export function get_comment_handlers(source, comments, index = 0) {
850
910
  const hasBlankLineAfter = /\n\s*\n/.test(sliceAfterComments);
851
911
 
852
912
  if (hasBlankLineAfter) {
853
- // Don't attach comments as trailing if next sibling is an Element
854
- // and any comment falls within the Element's line range
855
- // This means the comments are inside the Element (between opening and closing tags)
856
- const nextIsElement = nextSibling.type === 'Element';
913
+ // Don't attach comments as trailing if they are inside the next template node.
914
+ const nextIsElement = isNativeTemplateNode(nextSibling);
857
915
  const commentsInsideElement =
858
916
  nextIsElement &&
859
917
  nextSibling.loc &&
860
918
  comments.some((c) => {
861
919
  if (!c.loc) return false;
862
- // Check if comment is on a line between Element's start and end lines
920
+ // Check if comment is on a line between the JSX element's start and end lines
863
921
  return (
864
922
  c.loc.start.line >= nextSibling.loc.start.line &&
865
923
  c.loc.end.line <= nextSibling.loc.end.line