@tsrx/prettier-plugin 0.3.77 → 0.3.79

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsrx/prettier-plugin",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -27,7 +27,7 @@
27
27
  "prettier": "^3.8.3"
28
28
  },
29
29
  "dependencies": {
30
- "@tsrx/core": "0.1.25"
30
+ "@tsrx/core": "0.1.27"
31
31
  },
32
32
  "files": [
33
33
  "src/"
package/src/index.js CHANGED
@@ -2292,20 +2292,20 @@ function printRippleNode(node, path, options, print, args) {
2292
2292
  break;
2293
2293
 
2294
2294
  case 'JSXStyleElement':
2295
- nodeContent = printJSXElement(
2296
- /** @type {ESTreeJSX.JSXElement} */ (/** @type {unknown} */ (node)),
2297
- path,
2298
- options,
2299
- print,
2300
- );
2295
+ nodeContent = printJSXElement(node, path, options, print);
2301
2296
  break;
2302
2297
 
2303
2298
  case 'JSXElement':
2304
- nodeContent = printJSXElement(node, path, options, print);
2299
+ nodeContent = printJSXElement(/** @type {AST.TSRXJSXElement} */ (node), path, options, print);
2305
2300
  break;
2306
2301
 
2307
2302
  case 'JSXFragment':
2308
- nodeContent = printJSXFragment(node, path, options, print);
2303
+ nodeContent = printJSXFragment(
2304
+ /** @type {AST.TSRXJSXFragment} */ (node),
2305
+ path,
2306
+ options,
2307
+ print,
2308
+ );
2309
2309
  break;
2310
2310
 
2311
2311
  case 'JSXText':
@@ -5694,86 +5694,6 @@ function normalizeInlineJSXText(raw) {
5694
5694
  return text.trim() || !/[\r\n]/u.test(text) ? text : '';
5695
5695
  }
5696
5696
 
5697
- /**
5698
- * @param {AST.Node} parentNode
5699
- * @param {AST.Node} firstChild
5700
- * @param {Doc} childDoc
5701
- * @returns {boolean}
5702
- */
5703
- function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
5704
- if (!firstChild || childDoc == null) {
5705
- return false;
5706
- }
5707
-
5708
- if (firstChild.type === 'JSXText') {
5709
- return true;
5710
- }
5711
-
5712
- if (typeof childDoc === 'string') {
5713
- return childDoc.length <= 20 && !childDoc.includes('\n');
5714
- }
5715
-
5716
- // Inline JSX expressions if they fit, but respect original multi-line formatting
5717
- // for non-literal expressions (e.g. {children} should stay multi-line if written that way)
5718
- if (firstChild.type === 'JSXExpressionContainer') {
5719
- if (wasOriginallySingleLine(parentNode)) {
5720
- return true;
5721
- }
5722
- // For multi-line parents, only inline if the expression is a simple literal
5723
- const expr = firstChild.expression;
5724
- if (expr && (expr.type === 'Literal' || expr.type === 'TemplateLiteral')) {
5725
- return true;
5726
- }
5727
- return false;
5728
- }
5729
-
5730
- // Respect original formatting for elements: if parent was originally multi-line, keep it multi-line
5731
- // This follows Prettier's philosophy for decorators and objects
5732
- if (!wasOriginallySingleLine(parentNode)) {
5733
- return false;
5734
- }
5735
-
5736
- if (firstChild.type === 'JSXElement' && firstChild.openingElement?.selfClosing) {
5737
- const parent = /** @type {any} */ (parentNode);
5738
- return !parent.openingElement?.attributes?.length;
5739
- }
5740
-
5741
- return false;
5742
- }
5743
-
5744
- /**
5745
- * Check whether a child can participate in compact inline TSRX content.
5746
- * @param {any} child
5747
- * @returns {boolean}
5748
- */
5749
- function isInlineableTextOrExpressionChild(child) {
5750
- if (!child || (child.type !== 'JSXText' && child.type !== 'JSXExpressionContainer')) {
5751
- return false;
5752
- }
5753
-
5754
- if (hasComment(/** @type {AST.Node & AST.NodeWithMaybeComments} */ (child))) {
5755
- return false;
5756
- }
5757
-
5758
- const expression = /** @type {{ expression?: AST.Node & AST.NodeWithMaybeComments }} */ (child)
5759
- .expression;
5760
- return !expression || !hasComment(expression);
5761
- }
5762
-
5763
- /**
5764
- * @param {any} node
5765
- * @returns {boolean}
5766
- */
5767
- function shouldTryInlineMultipleTextChildren(node) {
5768
- return (
5769
- wasOriginallySingleLine(node) &&
5770
- Array.isArray(node.children) &&
5771
- node.children.length > 1 &&
5772
- node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
5773
- node.children.every(isInlineableTextOrExpressionChild)
5774
- );
5775
- }
5776
-
5777
5697
  /**
5778
5698
  * @param {AST.Node} child
5779
5699
  * @returns {boolean}
@@ -5795,72 +5715,10 @@ function isSimpleJSXExpressionChild(child) {
5795
5715
  );
5796
5716
  }
5797
5717
 
5798
- /**
5799
- * Get leading comments from element metadata
5800
- * @param {ESTreeJSX.JSXElement} node - The element node
5801
- * @returns {AST.Comment[]}
5802
- */
5803
- function getElementLeadingComments(node) {
5804
- const fromMetadata = node?.metadata?.elementLeadingComments;
5805
- if (Array.isArray(fromMetadata)) {
5806
- return fromMetadata;
5807
- }
5808
- return [];
5809
- }
5810
-
5811
- /**
5812
- * Create doc parts for element-level comments
5813
- * @param {AST.Comment[]} comments - Array of comments
5814
- * @returns {Doc[]}
5815
- */
5816
- function createElementLevelCommentParts(comments) {
5817
- if (!comments || comments.length === 0) {
5818
- return [];
5819
- }
5820
-
5821
- /** @type {Doc[]} */
5822
- const parts = [];
5823
-
5824
- for (let i = 0; i < comments.length; i++) {
5825
- const comment = comments[i];
5826
- const nextComment = comments[i + 1];
5827
-
5828
- if (comment.type === 'Line') {
5829
- parts.push('//' + comment.value);
5830
- parts.push(hardline);
5831
- } else if (comment.type === 'Block') {
5832
- parts.push('/*' + comment.value + '*/');
5833
- parts.push(hardline);
5834
- }
5835
-
5836
- if (nextComment) {
5837
- const blankLinesBetween = getBlankLinesBetweenNodes(comment, nextComment);
5838
- if (blankLinesBetween > 0) {
5839
- parts.push(hardline);
5840
- }
5841
- }
5842
- }
5843
-
5844
- return parts;
5845
- }
5846
-
5847
- /**
5848
- * Create element-level comment parts with trailing hardline trimmed
5849
- * @param {AST.Comment[]} comments - Array of comments
5850
- * @returns {Doc[]}
5851
- */
5852
- function createElementLevelCommentPartsTrimmed(comments) {
5853
- const parts = createElementLevelCommentParts(comments);
5854
- if (parts.length > 0 && parts[parts.length - 1] === hardline) {
5855
- parts.pop();
5856
- }
5857
- return parts;
5858
- }
5859
-
5860
5718
  /**
5861
5719
  * Print a JSX element
5862
- * @param {AST.TSRXJSXElement} node - The JSX element node
5863
- * @param {AstPath<any>} path - The AST path
5720
+ * @param {AST.TSRXJSXElement | AST.JSXStyleElement} node - The JSX element node
5721
+ * @param {AstPath<AST.TSRXJSXElement>} path - The AST path
5864
5722
  * @param {RippleFormatOptions} options - Prettier options
5865
5723
  * @param {PrintFn} print - Print callback
5866
5724
  * @returns {Doc | Doc[]}
@@ -5870,7 +5728,12 @@ function printJSXElement(node, path, options, print) {
5870
5728
  const openingElement = node.openingElement;
5871
5729
  const closingElement = node.closingElement;
5872
5730
 
5873
- const tagName = printJSXElementName(openingElement.name);
5731
+ // Dynamic tags (`<{expr}>`) print the opening expression for both tags so
5732
+ // they stay textually identical; static names print as plain strings.
5733
+ const tagName =
5734
+ openingElement.name.type === 'JSXExpressionContainer'
5735
+ ? ['{', path.call(print, 'openingElement', 'name', 'expression'), '}']
5736
+ : printJSXElementName(openingElement.name);
5874
5737
 
5875
5738
  const isSelfClosing = openingElement.selfClosing;
5876
5739
  const hasAttributes = openingElement.attributes && openingElement.attributes.length > 0;
@@ -5955,26 +5818,13 @@ function printJSXElement(node, path, options, print) {
5955
5818
  { shouldBreak: shouldForceBreak },
5956
5819
  );
5957
5820
 
5958
- // Trailing comments after the last child are attached by the parser either to
5959
- // the closing tag (`closingElement.leadingComments`) or, when the last child is
5960
- // an `{expr}` container, to `metadata.elementLeadingComments` positioned inside
5961
- // the body (start >= opening tag end). Emit both before `</tag>`.
5962
- const openingTagEnd = /** @type {AST.NodeWithLocation} */ (openingElement).end;
5963
- const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
5964
- (/** @type {AST.Comment} */ comment) =>
5965
- typeof comment.start === 'number' && comment.start >= openingTagEnd,
5821
+ // Comments before `</tag>` and the comments of a comment-only element.
5822
+ const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
5823
+ /** @type {AST.TSRXJSXElement} */ (node),
5824
+ openingElement,
5825
+ node.closingElement,
5966
5826
  );
5967
- const trailingComments = [
5968
- ...(node.closingElement?.leadingComments ?? []),
5969
- ...bodyMetaComments,
5970
- ].sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
5971
- const lastMeaningfulChild = [...(node.children ?? [])]
5972
- .reverse()
5973
- .find((child) => child.type !== 'JSXText' || child.value.trim());
5974
- const closingCommentDocs = printElementBodyLineComments(trailingComments, lastMeaningfulChild);
5975
5827
  const hasClosingComments = closingCommentDocs.length > 0;
5976
- // A comment-only element has no children; its comments live in `innerComments`.
5977
- const innerCommentDocs = printElementBodyLineComments(node.innerComments);
5978
5828
 
5979
5829
  if (!hasChildren) {
5980
5830
  const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
@@ -6107,7 +5957,7 @@ function printJSXElement(node, path, options, print) {
6107
5957
  ];
6108
5958
  }
6109
5959
  const meaningfulChildren = node.children.filter(
6110
- (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim(),
5960
+ (child) => child.type !== 'JSXText' || child.value.trim(),
6111
5961
  );
6112
5962
  const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
6113
5963
  if (
@@ -6122,9 +5972,9 @@ function printJSXElement(node, path, options, print) {
6122
5972
  !forceMultiline &&
6123
5973
  childrenDocs.length > 1 &&
6124
5974
  wasOriginallySingleLine(node) &&
6125
- node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
5975
+ node.children.some((child) => child.type === 'JSXText') &&
6126
5976
  node.children.every(
6127
- (/** @type {any} */ child) =>
5977
+ (child) =>
6128
5978
  child.type === 'JSXText' || isSimpleJSXExpressionChild(/** @type {AST.Node} */ (child)),
6129
5979
  )
6130
5980
  ) {
@@ -6157,8 +6007,8 @@ function printJSXElement(node, path, options, print) {
6157
6007
 
6158
6008
  /**
6159
6009
  * Print a JSX fragment (<>...</>)
6160
- * @param {ESTreeJSX.JSXFragment} node - The JSX fragment node
6161
- * @param {AstPath<ESTreeJSX.JSXFragment>} path - The AST path
6010
+ * @param {AST.TSRXJSXFragment} node - The JSX fragment node
6011
+ * @param {AstPath<AST.TSRXJSXFragment>} path - The AST path
6162
6012
  * @param {RippleFormatOptions} options - Prettier options
6163
6013
  * @param {PrintFn} print - Print callback
6164
6014
  * @returns {Doc}
@@ -6166,12 +6016,23 @@ function printJSXElement(node, path, options, print) {
6166
6016
  function printJSXFragment(node, path, options, print) {
6167
6017
  const hasChildren = node.children && node.children.length > 0;
6168
6018
 
6019
+ // Comments before `</>` and the comments of a comment-only fragment.
6020
+ const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
6021
+ node,
6022
+ node.openingFragment,
6023
+ node.closingFragment,
6024
+ );
6025
+
6169
6026
  if (!hasChildren) {
6027
+ const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
6028
+ if (bodyComments.length > 0) {
6029
+ return group(['<>', indent(bodyComments), hardline, '</>']);
6030
+ }
6170
6031
  return '<></>';
6171
6032
  }
6172
6033
 
6173
6034
  // A `@{ … }` code block is the whole body and hugs the tags: `<>@{ … }</>`.
6174
- if (node.children.length === 1 && /** @type {any} */ (node.children[0]).type === 'JSXCodeBlock') {
6035
+ if (node.children.length === 1 && node.children[0].type === 'JSXCodeBlock') {
6175
6036
  return group(['<>', path.call(print, 'children', 0), '</>']);
6176
6037
  }
6177
6038
 
@@ -6214,7 +6075,11 @@ function printJSXFragment(node, path, options, print) {
6214
6075
  }
6215
6076
 
6216
6077
  // Check if content can be inlined (single text node or single expression)
6217
- if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
6078
+ if (
6079
+ childrenDocs.length === 1 &&
6080
+ typeof childrenDocs[0] === 'string' &&
6081
+ closingCommentDocs.length === 0
6082
+ ) {
6218
6083
  return ['<>', childrenDocs[0], '</>'];
6219
6084
  }
6220
6085
  const meaningfulChildren = node.children.filter(
@@ -6225,6 +6090,7 @@ function printJSXFragment(node, path, options, print) {
6225
6090
  meaningfulChildren.length === 1 &&
6226
6091
  meaningfulChildren[0].type === 'JSXElement' &&
6227
6092
  wasOriginallySingleLine(node) &&
6093
+ closingCommentDocs.length === 0 &&
6228
6094
  !willBreak(childrenDocs[0])
6229
6095
  ) {
6230
6096
  // Keep the fragment inline when it fits; otherwise expand `<>` onto its own
@@ -6248,7 +6114,12 @@ function printJSXFragment(node, path, options, print) {
6248
6114
  }
6249
6115
 
6250
6116
  // Build the final fragment
6251
- return group(['<>', indent([hardline, ...formattedChildren]), hardline, '</>']);
6117
+ return group([
6118
+ '<>',
6119
+ indent([hardline, ...formattedChildren, ...closingCommentDocs]),
6120
+ hardline,
6121
+ '</>',
6122
+ ]);
6252
6123
  }
6253
6124
 
6254
6125
  /**
@@ -6257,14 +6128,14 @@ function printJSXFragment(node, path, options, print) {
6257
6128
  * opening tag's end, but the child is visited first). Pull those out of the
6258
6129
  * children and return a map from attribute index to the comments that precede it,
6259
6130
  * so the element printer can render them in the opening tag instead of the body.
6260
- * @param {AST.TSRXJSXElement} node
6131
+ * @param {AST.TSRXJSXElement | AST.JSXStyleElement} node
6261
6132
  * @returns {Map<number, AST.Comment[]>}
6262
6133
  */
6263
6134
  function collectOpeningTagComments(node) {
6264
6135
  /** @type {Map<number, AST.Comment[]>} */
6265
6136
  const byAttr = new Map();
6266
6137
  const openingElement = /** @type {AST.NodeWithLocation} */ (node.openingElement);
6267
- const attributes = /** @type {any[]} */ (node.openingElement?.attributes) ?? [];
6138
+ const attributes = node.openingElement?.attributes ?? [];
6268
6139
  if (!openingElement || attributes.length === 0 || !Array.isArray(node.children)) {
6269
6140
  return byAttr;
6270
6141
  }
@@ -6283,7 +6154,7 @@ function collectOpeningTagComments(node) {
6283
6154
  }
6284
6155
  }
6285
6156
  if (keep.length !== lead.length) {
6286
- /** @type {any} */ (child).leadingComments = keep;
6157
+ child.leadingComments = keep;
6287
6158
  }
6288
6159
  }
6289
6160
  if (collected.length === 0) return byAttr;
@@ -6333,18 +6204,48 @@ function printTemplateChildLeadingComments(child) {
6333
6204
  }
6334
6205
 
6335
6206
  /**
6336
- * Build doc parts for `//` line comments attached to an element body — trailing
6207
+ * Collect and print the comments that belong to an element/fragment body:
6208
+ * trailing comments after the last child (attached by the parser to the closing
6209
+ * tag's `leadingComments` or, when the last child is an `{expr}` container, to
6210
+ * `metadata.elementLeadingComments` positioned inside the body) and the comments
6211
+ * of a comment-only body (`innerComments`).
6212
+ * @param {AST.TSRXJSXElement | AST.TSRXJSXFragment} node
6213
+ * @param {AST.TSRXJSXElement['openingElement'] | AST.TSRXJSXFragment['openingFragment']} openingNode
6214
+ * @param {AST.TSRXJSXElement['closingElement'] | AST.TSRXJSXFragment['closingFragment']} closingNode
6215
+ * @returns {{ closingCommentDocs: Doc[], innerCommentDocs: Doc[] }}
6216
+ */
6217
+ function collectElementBodyCommentDocs(node, openingNode, closingNode) {
6218
+ const openingEnd = openingNode?.end;
6219
+ const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
6220
+ (/** @type {AST.Comment} */ comment) =>
6221
+ typeof comment.start === 'number' &&
6222
+ typeof openingEnd === 'number' &&
6223
+ comment.start >= openingEnd,
6224
+ );
6225
+ const trailingComments = [...(closingNode?.leadingComments ?? []), ...bodyMetaComments].sort(
6226
+ (/** @type {AST.Comment} */ a, /** @type {AST.Comment} */ b) =>
6227
+ /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
6228
+ );
6229
+ const lastMeaningfulChild = [...(node.children ?? [])]
6230
+ .reverse()
6231
+ .find((child) => child.type !== 'JSXText' || child.value.trim());
6232
+ return {
6233
+ closingCommentDocs: printElementBodyComments(trailingComments, lastMeaningfulChild),
6234
+ innerCommentDocs: printElementBodyComments(node.innerComments),
6235
+ };
6236
+ }
6237
+
6238
+ /**
6239
+ * Build doc parts for comments attached to an element body — trailing
6337
6240
  * comments before `</tag>` (`closingElement.leadingComments`) or the comments of a
6338
- * comment-only element (`innerComments`). Block comments are intentionally skipped:
6339
- * they survive in the adjacent JSXText value and are already rendered as text, so
6340
- * emitting them here would duplicate them. Each comment is emitted on its own line
6241
+ * comment-only element (`innerComments`). Each comment is emitted on its own line
6341
6242
  * at the children indent.
6342
6243
  * @param {AST.Comment[] | null | undefined} commentList
6343
6244
  * @param {any} [previousNode]
6344
6245
  * @returns {Doc[]}
6345
6246
  */
6346
- function printElementBodyLineComments(commentList, previousNode = null) {
6347
- const comments = (commentList ?? []).filter((comment) => comment.type === 'Line');
6247
+ function printElementBodyComments(commentList, previousNode = null) {
6248
+ const comments = commentList ?? [];
6348
6249
  if (comments.length === 0) {
6349
6250
  return [];
6350
6251
  }
@@ -6358,7 +6259,9 @@ function printElementBodyLineComments(commentList, previousNode = null) {
6358
6259
  if (prev && getBlankLinesBetweenNodes(prev, comments[i]) > 0) {
6359
6260
  parts.push(hardline);
6360
6261
  }
6361
- parts.push('//' + comments[i].value);
6262
+ parts.push(
6263
+ comments[i].type === 'Line' ? '//' + comments[i].value : '/*' + comments[i].value + '*/',
6264
+ );
6362
6265
  prev = comments[i];
6363
6266
  }
6364
6267
  return parts;
@@ -6399,10 +6302,10 @@ function printJSXCodeBlock(node, path, options, print) {
6399
6302
  parts.push(path.call(print, 'render'));
6400
6303
  }
6401
6304
  // Trailing comments after the last statement/render inside the block.
6402
- const innerCommentDocs = printElementBodyLineComments(node.innerComments);
6305
+ const innerCommentDocs = printElementBodyComments(node.innerComments);
6403
6306
  if (innerCommentDocs.length > 0) {
6404
6307
  const lastNode = node.render ?? node.body[node.body.length - 1];
6405
- const firstComment = (node.innerComments ?? []).find((c) => c.type === 'Line');
6308
+ const firstComment = (node.innerComments ?? [])[0];
6406
6309
  if (lastNode && firstComment && getBlankLinesBetweenNodes(lastNode, firstComment) > 0) {
6407
6310
  parts.push(hardline);
6408
6311
  }
@@ -6483,539 +6386,3 @@ function printJSXElementName(node) {
6483
6386
  }
6484
6387
  return 'Unknown';
6485
6388
  }
6486
-
6487
- /**
6488
- * Print a member expression as simple string (for element tag names)
6489
- * @param {AST.Node} node - The node to print
6490
- * @param {RippleFormatOptions} options - Prettier options
6491
- * @param {boolean} [computed=false] - Whether the property is computed
6492
- * @returns {string}
6493
- */
6494
- function printMemberExpressionSimple(node, options, computed = false) {
6495
- if (node.type === 'JSXIdentifier') {
6496
- return node.name;
6497
- }
6498
-
6499
- if (node.type === 'JSXMemberExpression') {
6500
- return (
6501
- printMemberExpressionSimple(node.object, options) +
6502
- '.' +
6503
- printMemberExpressionSimple(node.property, options, true)
6504
- );
6505
- }
6506
-
6507
- if (node.type === 'JSXNamespacedName') {
6508
- return node.namespace.name + ':' + node.name.name;
6509
- }
6510
-
6511
- if (node.type === 'Identifier') {
6512
- return node.name;
6513
- }
6514
-
6515
- if (node.type === 'MemberExpression') {
6516
- const obj = printMemberExpressionSimple(node.object, options);
6517
- let prop;
6518
- if (node.computed) {
6519
- prop = '[' + printMemberExpressionSimple(node.property, options, true) + ']';
6520
- } else {
6521
- prop = '.' + printMemberExpressionSimple(node.property, options, true);
6522
- }
6523
- return obj + prop;
6524
- }
6525
-
6526
- if (node.type === 'Literal') {
6527
- return computed ? formatStringLiteral(node.value, options) : JSON.stringify(node.value);
6528
- }
6529
- return '';
6530
- }
6531
-
6532
- /**
6533
- * Check whether an attribute value can expand into multiline content.
6534
- * @param {AST.Expression | null | undefined} value - The attribute value node
6535
- * @param {boolean} [is_nested_in_object=false] - Whether this value is nested within an object literal
6536
- * @returns {boolean}
6537
- */
6538
- function is_attribute_value_breakable(value, is_nested_in_object = false) {
6539
- if (!value) return false;
6540
-
6541
- switch (value.type) {
6542
- case 'ConditionalExpression':
6543
- // Keep simple top-level ternary attributes inline when they fit.
6544
- // We only force-break when a conditional is nested in an object literal value.
6545
- return is_nested_in_object;
6546
- case 'ObjectExpression':
6547
- return value.properties.some(
6548
- (property) =>
6549
- property.type === 'Property' &&
6550
- is_attribute_value_breakable(/** @type {AST.Expression} */ (property.value), true),
6551
- );
6552
- default:
6553
- return false;
6554
- }
6555
- }
6556
-
6557
- /**
6558
- * Print a JSX element node
6559
- * @param {AST.Element} element - The element node
6560
- * @param {AstPath<AST.Element>} path - The AST path
6561
- * @param {RippleFormatOptions} options - Prettier options
6562
- * @param {PrintFn} print - Print callback
6563
- * @returns {Doc}
6564
- */
6565
- function printElement(element, path, options, print) {
6566
- const node = /** @type {any} */ (element);
6567
- const tagName = printMemberExpressionSimple(node.id, options);
6568
- const openingElement = /** @type {any} */ (node.openingElement);
6569
- /** @type {Doc} */
6570
- let typeArgsDoc = '';
6571
- if (openingElement?.typeArguments) {
6572
- typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
6573
- }
6574
- const elementLeadingComments = getElementLeadingComments(/** @type {any} */ (node));
6575
-
6576
- // `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
6577
- // body (after the opening tag). Those must not be hoisted before the element.
6578
- const outerElementLeadingComments = elementLeadingComments.filter(
6579
- (/** @type {AST.Comment} */ comment) =>
6580
- typeof comment.start !== 'number' || comment.start < node.start,
6581
- );
6582
- const innerElementBodyComments = elementLeadingComments.filter(
6583
- (/** @type {AST.Comment} */ comment) =>
6584
- typeof comment.start === 'number' &&
6585
- comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
6586
- comment.start < node.end,
6587
- );
6588
- const metadataCommentParts =
6589
- outerElementLeadingComments.length > 0
6590
- ? createElementLevelCommentParts(outerElementLeadingComments)
6591
- : [];
6592
- const fallbackElementComments = [];
6593
- const shouldLiftTextLevelComments = outerElementLeadingComments.length === 0;
6594
-
6595
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
6596
- const hasInnerComments = Array.isArray(node.innerComments) && node.innerComments.length > 0;
6597
- const isSelfClosing = !!node.selfClosing;
6598
- const hasAttributes = Array.isArray(node.attributes) && node.attributes.length > 0;
6599
-
6600
- // Collect comments that the parser attached to children but actually belong inside
6601
- // the opening tag (positionally before openingElement.end). These should be printed
6602
- // as leading comments before the appropriate attribute, not lifted to element-level.
6603
- /** @type {Set<AST.Comment>} */
6604
- const openingTagCommentsSet = new Set();
6605
- if (hasChildren && node.openingElement) {
6606
- const openingEnd = /** @type {AST.NodeWithLocation} */ (node.openingElement).end;
6607
- for (const child of node.children) {
6608
- if (
6609
- (child.type === 'JSXExpressionContainer' || child.type === 'JSXText') &&
6610
- Array.isArray(child.leadingComments)
6611
- ) {
6612
- for (const comment of child.leadingComments) {
6613
- if (
6614
- typeof comment.start === 'number' &&
6615
- comment.start >= node.start &&
6616
- comment.start < openingEnd
6617
- ) {
6618
- openingTagCommentsSet.add(comment);
6619
- }
6620
- }
6621
- }
6622
- }
6623
- }
6624
-
6625
- // Build a map from attribute index to comments that should precede that attribute
6626
- const openingTagCommentsByAttrIndex = new Map();
6627
- if (openingTagCommentsSet.size > 0 && hasAttributes) {
6628
- const sortedOTC = [...openingTagCommentsSet].sort(
6629
- (a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
6630
- );
6631
- let commentIdx = 0;
6632
- for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++) {
6633
- const attr = node.attributes[attrIdx];
6634
- const commentsForAttr = [];
6635
- while (
6636
- commentIdx < sortedOTC.length &&
6637
- /** @type {number} */ (sortedOTC[commentIdx].start) <
6638
- /** @type {AST.NodeWithLocation} */ (attr).start
6639
- ) {
6640
- commentsForAttr.push(sortedOTC[commentIdx]);
6641
- commentIdx++;
6642
- }
6643
- if (commentsForAttr.length > 0) {
6644
- openingTagCommentsByAttrIndex.set(attrIdx, commentsForAttr);
6645
- }
6646
- }
6647
- }
6648
-
6649
- if (isSelfClosing && !hasInnerComments && !hasAttributes) {
6650
- const elementDoc = group(['<', tagName, typeArgsDoc, ' />']);
6651
- return metadataCommentParts.length > 0 ? [...metadataCommentParts, elementDoc] : elementDoc;
6652
- }
6653
-
6654
- // Determine the line break type for attributes
6655
- // When singleAttributePerLine is true, force each attribute on its own line with hardline
6656
- // Otherwise, use line to allow collapsing when it fits
6657
- const attrLineBreak = options.singleAttributePerLine ? hardline : line;
6658
-
6659
- const shouldUseSelfClosingSyntax = isSelfClosing || (!hasChildren && !hasInnerComments);
6660
-
6661
- const hasOpeningTagComments = openingTagCommentsSet.size > 0;
6662
- let attrIndex = 0;
6663
- let hasBreakingAttribute = false;
6664
- const attrDocs = hasAttributes
6665
- ? path.map((attrPath) => {
6666
- const idx = attrIndex++;
6667
- const commentsForAttr = openingTagCommentsByAttrIndex.get(idx);
6668
- /** @type {Doc[]} */
6669
- const parts = [];
6670
- if (commentsForAttr) {
6671
- for (const comment of commentsForAttr) {
6672
- // Line comments (//) consume the rest of the line, so they must
6673
- // use hardline to force a break. Block comments can use normal breaks.
6674
- if (comment.type === 'Line') {
6675
- parts.push(hardline);
6676
- parts.push('//' + comment.value);
6677
- } else if (comment.type === 'Block') {
6678
- parts.push(attrLineBreak);
6679
- parts.push('/*' + comment.value + '*/');
6680
- }
6681
- }
6682
- }
6683
- parts.push(attrLineBreak);
6684
- const attrDoc = print(attrPath);
6685
- parts.push(attrDoc);
6686
- const attr_node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
6687
- /** @type {unknown} */ (attrPath.node)
6688
- );
6689
- if (
6690
- !hasBreakingAttribute &&
6691
- (willBreak(attrDoc) ||
6692
- (attr_node.type === 'JSXAttribute' &&
6693
- is_attribute_value_breakable(/** @type {any} */ (attr_node.value))))
6694
- ) {
6695
- hasBreakingAttribute = true;
6696
- }
6697
- return parts;
6698
- }, 'attributes')
6699
- : [];
6700
- const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
6701
- const openingTagAlwaysBreaks =
6702
- (hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
6703
- const openingTag = group([
6704
- '<',
6705
- tagName,
6706
- typeArgsDoc,
6707
- hasAttributes
6708
- ? indent([
6709
- ...attrDocs,
6710
- // Force the group to break when there are line comments in the opening tag,
6711
- // or when any attribute value would break (e.g. multiline objects, ternaries).
6712
- // This ensures attributes are broken onto separate lines rather than breaking
6713
- // expression values inline on the same line as the tag name.
6714
- ...(shouldForceBreak ? [breakParent] : []),
6715
- ])
6716
- : '',
6717
- // Add line break opportunity before > or />
6718
- // Use line for self-closing (keeps space), softline for non-self-closing when attributes present
6719
- // When bracketSameLine is true, don't add line break for non-self-closing elements
6720
- shouldUseSelfClosingSyntax
6721
- ? hasAttributes
6722
- ? line
6723
- : ''
6724
- : hasAttributes && !options.bracketSameLine
6725
- ? softline
6726
- : '',
6727
- shouldUseSelfClosingSyntax ? (hasAttributes ? '/>' : ' />') : '>',
6728
- ]);
6729
-
6730
- if (!hasChildren) {
6731
- if (!hasInnerComments) {
6732
- return metadataCommentParts.length > 0 ? [...metadataCommentParts, openingTag] : openingTag;
6733
- }
6734
-
6735
- /** @type {Doc[]} */
6736
- const innerParts = [];
6737
- for (const comment of node.innerComments ?? []) {
6738
- if (comment.type === 'Line') {
6739
- innerParts.push('//' + comment.value);
6740
- innerParts.push(hardline);
6741
- } else if (comment.type === 'Block') {
6742
- innerParts.push('/*' + comment.value + '*/');
6743
- innerParts.push(hardline);
6744
- }
6745
- }
6746
-
6747
- if (innerParts.length > 0 && innerParts[innerParts.length - 1] === hardline) {
6748
- innerParts.pop();
6749
- }
6750
-
6751
- const closingTag = ['</', tagName, '>'];
6752
- const elementOutput = group([
6753
- openingTag,
6754
- indent([hardline, ...innerParts]),
6755
- hardline,
6756
- closingTag,
6757
- ]);
6758
- return metadataCommentParts.length > 0
6759
- ? [...metadataCommentParts, elementOutput]
6760
- : elementOutput;
6761
- }
6762
-
6763
- // Has children - use unified children processing
6764
- // Build children with whitespace preservation
6765
- /** @type {Doc[]} */
6766
- const finalChildren = [];
6767
- const sortedInnerElementBodyComments =
6768
- innerElementBodyComments.length > 0
6769
- ? innerElementBodyComments.slice().sort((a, b) => (a.start ?? 0) - (b.start ?? 0))
6770
- : [];
6771
- let innerElementBodyCommentIndex = 0;
6772
-
6773
- for (let i = 0; i < node.children.length; i++) {
6774
- const currentChild = node.children[i];
6775
- const nextChild = node.children[i + 1];
6776
-
6777
- // Insert any element-body comments that appear before this child.
6778
- if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6779
- const currentChildStart = typeof currentChild.start === 'number' ? currentChild.start : null;
6780
- if (currentChildStart != null) {
6781
- const commentsBefore = [];
6782
- while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6783
- /** @type {AST.Comment} */
6784
- const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
6785
- if (typeof comment.start !== 'number' || comment.start >= currentChildStart) {
6786
- break;
6787
- }
6788
- commentsBefore.push(comment);
6789
- innerElementBodyCommentIndex++;
6790
- }
6791
- if (commentsBefore.length > 0) {
6792
- if (finalChildren.length > 0) {
6793
- finalChildren.push(hardline);
6794
- }
6795
- finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBefore));
6796
- finalChildren.push(hardline);
6797
- }
6798
- }
6799
- }
6800
-
6801
- const isTextLikeChild =
6802
- currentChild.type === 'JSXExpressionContainer' || currentChild.type === 'JSXText';
6803
- const hasTextLeadingComments =
6804
- shouldLiftTextLevelComments &&
6805
- isTextLikeChild &&
6806
- Array.isArray(currentChild.leadingComments) &&
6807
- currentChild.leadingComments.length > 0;
6808
- const currentChildAny = /** @type {any} */ (currentChild);
6809
- const rawExpressionLeadingComments =
6810
- isTextLikeChild && Array.isArray(currentChildAny.expression?.leadingComments)
6811
- ? currentChildAny.expression.leadingComments
6812
- : null;
6813
- const elementBodyLeadingComments =
6814
- hasTextLeadingComments && node.openingElement
6815
- ? /** @type {AST.Comment[]} */ (currentChild.leadingComments).filter(
6816
- (comment) =>
6817
- comment.context?.containerId === node.metadata?.commentContainerId &&
6818
- comment.context?.beforeMeaningfulChild &&
6819
- typeof comment.start === 'number' &&
6820
- comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
6821
- comment.start < /** @type {AST.NodeWithLocation} */ (currentChild).start,
6822
- )
6823
- : [];
6824
-
6825
- if (hasTextLeadingComments) {
6826
- for (let j = 0; j < /** @type {AST.Comment[]} */ (currentChild.leadingComments).length; j++) {
6827
- const comment = /** @type {AST.Comment[]} */ (currentChild.leadingComments)[j];
6828
- // Don't lift comments that belong inside the opening tag (handled in attribute section)
6829
- if (!openingTagCommentsSet.has(comment) && !elementBodyLeadingComments.includes(comment)) {
6830
- fallbackElementComments.push(comment);
6831
- }
6832
- }
6833
- }
6834
-
6835
- const childPrintArgs = /** @type {PrintArgs} */ ({});
6836
- if (hasTextLeadingComments) {
6837
- childPrintArgs.suppressLeadingComments = true;
6838
- }
6839
- if (rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0) {
6840
- childPrintArgs.suppressExpressionLeadingComments = true;
6841
- }
6842
-
6843
- const printedChild =
6844
- Object.keys(childPrintArgs).length > 0
6845
- ? path.call((childPath) => print(childPath, childPrintArgs), 'children', i)
6846
- : path.call(print, 'children', i);
6847
-
6848
- const childLeadingCommentParts =
6849
- elementBodyLeadingComments.length > 0
6850
- ? createElementLevelCommentParts(elementBodyLeadingComments)
6851
- : rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
6852
- ? createElementLevelCommentParts(rawExpressionLeadingComments)
6853
- : null;
6854
- const childDoc = childLeadingCommentParts
6855
- ? [...childLeadingCommentParts, printedChild]
6856
- : printedChild;
6857
- finalChildren.push(childDoc);
6858
-
6859
- // Insert element-body comments that fall between this child and the next child (or the closing tag).
6860
- let insertedBodyCommentsBetween = false;
6861
- if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6862
- const currentChildEnd = /** @type {AST.NodeWithLocation} */ (currentChild).end;
6863
- const nextChildStart =
6864
- nextChild && typeof nextChild.start === 'number' ? nextChild.start : null;
6865
- const commentsBetween = [];
6866
- while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6867
- /** @type {AST.Comment} */
6868
- const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
6869
- if (typeof comment.start !== 'number') {
6870
- innerElementBodyCommentIndex++;
6871
- continue;
6872
- }
6873
- if (comment.start < currentChildEnd) {
6874
- break;
6875
- }
6876
- if (nextChildStart != null && comment.start >= nextChildStart) {
6877
- break;
6878
- }
6879
- commentsBetween.push(comment);
6880
- innerElementBodyCommentIndex++;
6881
- }
6882
- if (commentsBetween.length > 0) {
6883
- const firstComment = commentsBetween[0];
6884
- const lastComment = commentsBetween[commentsBetween.length - 1];
6885
-
6886
- // Preserve any blank line(s) that existed between the previous child and the comment block.
6887
- const blankLinesBefore = getBlankLinesBetweenNodes(currentChild, firstComment);
6888
- finalChildren.push(hardline);
6889
- if (blankLinesBefore > 0) {
6890
- finalChildren.push(hardline);
6891
- }
6892
-
6893
- finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBetween));
6894
-
6895
- if (nextChild) {
6896
- // Preserve any blank line(s) that existed between the comment block and the next child.
6897
- const blankLinesAfter = getBlankLinesBetweenNodes(lastComment, nextChild);
6898
- finalChildren.push(hardline);
6899
- if (blankLinesAfter > 0) {
6900
- finalChildren.push(hardline);
6901
- }
6902
- }
6903
- insertedBodyCommentsBetween = true;
6904
- }
6905
- }
6906
-
6907
- if (nextChild) {
6908
- if (insertedBodyCommentsBetween) {
6909
- continue;
6910
- }
6911
- const whitespaceTarget =
6912
- nextChild.leadingComments && nextChild.leadingComments.length > 0
6913
- ? nextChild.leadingComments[0]
6914
- : nextChild;
6915
- const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
6916
- const isTextOrExpressionChild =
6917
- currentChild.type === 'JSXExpressionContainer' ||
6918
- currentChild.type === 'JSXText' ||
6919
- nextChild.type === 'JSXExpressionContainer' ||
6920
- nextChild.type === 'JSXText';
6921
-
6922
- if (whitespaceLinesCount > 0) {
6923
- finalChildren.push(hardline);
6924
- finalChildren.push(hardline);
6925
- } else if (!isTextOrExpressionChild && shouldAddBlankLine(currentChild, nextChild)) {
6926
- finalChildren.push(hardline);
6927
- finalChildren.push(hardline);
6928
- } else {
6929
- finalChildren.push(hardline);
6930
- }
6931
- }
6932
- }
6933
-
6934
- // Collect comments attached to the closing element (comments that appear after the last child
6935
- // but before the closing tag, e.g. `</div>`). The parser attaches these as leadingComments
6936
- // on either the closingElement node or the closingElement.name node depending on context.
6937
- const closingElementComments = [
6938
- ...(node.closingElement && Array.isArray(node.closingElement.leadingComments)
6939
- ? node.closingElement.leadingComments
6940
- : []),
6941
- ...(node.closingElement &&
6942
- node.closingElement.name &&
6943
- Array.isArray(node.closingElement.name.leadingComments)
6944
- ? node.closingElement.name.leadingComments
6945
- : []),
6946
- ];
6947
-
6948
- if (closingElementComments.length > 0) {
6949
- const lastChild = node.children[node.children.length - 1];
6950
- if (lastChild) {
6951
- const blankLinesBefore = getBlankLinesBetweenNodes(lastChild, closingElementComments[0]);
6952
- finalChildren.push(hardline);
6953
- if (blankLinesBefore > 0) {
6954
- finalChildren.push(hardline);
6955
- }
6956
- }
6957
- finalChildren.push(...createElementLevelCommentPartsTrimmed(closingElementComments));
6958
- }
6959
-
6960
- const fallbackCommentParts =
6961
- fallbackElementComments.length > 0
6962
- ? createElementLevelCommentParts(fallbackElementComments)
6963
- : [];
6964
- const leadingCommentParts =
6965
- metadataCommentParts.length > 0
6966
- ? [...metadataCommentParts, ...fallbackCommentParts]
6967
- : fallbackCommentParts;
6968
-
6969
- const closingTag = ['</', tagName, '>'];
6970
- let elementOutput;
6971
- const shouldTryInlineMultipleChildren =
6972
- !openingTagAlwaysBreaks &&
6973
- fallbackCommentParts.length === 0 &&
6974
- closingElementComments.length === 0 &&
6975
- shouldTryInlineMultipleTextChildren(node);
6976
-
6977
- if (finalChildren.length === 1) {
6978
- const child = finalChildren[0];
6979
- const firstChild = /** @type {any} */ (node.children[0]);
6980
- const isNonSelfClosingElement =
6981
- firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6982
- const isElementChild = firstChild && firstChild.type === 'Element';
6983
- const isRawTextChild =
6984
- firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
6985
-
6986
- if (
6987
- (typeof child === 'string' || isRawTextChild) &&
6988
- shouldInlineSingleChild(node, firstChild, child)
6989
- ) {
6990
- elementOutput = openingTagAlwaysBreaks
6991
- ? [openingTag, indent([hardline, child]), hardline, closingTag]
6992
- : conditionalGroup([
6993
- group([openingTag, child, closingTag]),
6994
- [openingTag, indent([hardline, child]), hardline, closingTag],
6995
- ]);
6996
- } else if (
6997
- child &&
6998
- typeof child === 'object' &&
6999
- !isNonSelfClosingElement &&
7000
- shouldInlineSingleChild(node, firstChild, child)
7001
- ) {
7002
- if (isElementChild && hasAttributes) {
7003
- elementOutput = [openingTag, indent([hardline, child]), hardline, closingTag];
7004
- } else {
7005
- elementOutput = group([openingTag, indent([softline, child]), softline, closingTag]);
7006
- }
7007
- } else {
7008
- elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
7009
- }
7010
- } else if (shouldTryInlineMultipleChildren) {
7011
- const inlineChildren = path.map(print, 'children');
7012
- elementOutput = conditionalGroup([
7013
- group([openingTag, ...inlineChildren, closingTag]),
7014
- [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag],
7015
- ]);
7016
- } else {
7017
- elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
7018
- }
7019
-
7020
- return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
7021
- }
package/src/index.test.js CHANGED
@@ -74,6 +74,34 @@ describe('prettier-plugin', () => {
74
74
  expect(result).toBeWithNewline(expected);
75
75
  });
76
76
 
77
+ it('formats dynamic element tags', async () => {
78
+ const input = `function App(props){const Child='div';return <{Child} {...props} class="card"><span>Hello</span></{Child}>}`;
79
+ const expected = `function App(props) {
80
+ const Child = "div";
81
+ return <{Child} {...props} class="card">
82
+ <span>Hello</span>
83
+ </{Child}>;
84
+ }`;
85
+
86
+ const result = await format(input);
87
+ expect(result).toBeWithNewline(expected);
88
+ });
89
+
90
+ it('formats dynamic element tag expressions', async () => {
91
+ const input = `function App(){return <><{registry.item}/><{items[0]}/><{'section'}/><{\`article\`}/></>;}`;
92
+ const expected = `function App() {
93
+ return <>
94
+ <{registry.item} />
95
+ <{items[0]} />
96
+ <{"section"} />
97
+ <{\`article\`} />
98
+ </>;
99
+ }`;
100
+
101
+ const result = await format(input);
102
+ expect(result).toBeWithNewline(expected);
103
+ });
104
+
77
105
  it('formats a fragment code block with setup and template control flow', async () => {
78
106
  const input = `function App(){return <>@{
79
107
  const items=[1,2,3];
@@ -2725,6 +2753,104 @@ function test() {
2725
2753
  expect(result).toBeWithNewline(expected);
2726
2754
  });
2727
2755
 
2756
+ it('should preserve template comments and blank lines from unformatted input', async () => {
2757
+ const input = `function TodoList() @{
2758
+ <>
2759
+ /* world 0 */
2760
+ // hello
2761
+ /* world 1 */
2762
+ <ul>
2763
+ // hello
2764
+ /* world 2 */
2765
+
2766
+ </ul>
2767
+
2768
+ <ul>
2769
+ // hello
2770
+ /* world 3 */
2771
+ // hello
2772
+ </ul>
2773
+ /* world 4 */
2774
+ </>
2775
+ }`;
2776
+
2777
+ const expected = `function TodoList() @{
2778
+ <>
2779
+ /* world 0 */
2780
+ // hello
2781
+ /* world 1 */
2782
+ <ul>
2783
+ // hello
2784
+ /* world 2 */
2785
+ </ul>
2786
+
2787
+ <ul>
2788
+ // hello
2789
+ /* world 3 */
2790
+ // hello
2791
+ </ul>
2792
+ /* world 4 */
2793
+ </>
2794
+ }`;
2795
+
2796
+ const result = await format(input, { singleQuote: true });
2797
+ expect(result).toBeWithNewline(expected);
2798
+ // Reformatting the output must be stable.
2799
+ expect(await format(result, { singleQuote: true })).toBeWithNewline(expected);
2800
+ });
2801
+
2802
+ it('should preserve block comments before a closing fragment', async () => {
2803
+ const expected = `function App() @{
2804
+ <>
2805
+ <span>{'child'}</span>
2806
+
2807
+ /* block comment */
2808
+ </>
2809
+ }`;
2810
+
2811
+ const result = await format(expected, { singleQuote: true });
2812
+ expect(result).toBeWithNewline(expected);
2813
+ });
2814
+
2815
+ it('should preserve a blank line before a trailing block comment in elements', async () => {
2816
+ const expected = `function App() {
2817
+ <div>
2818
+ <span>{'child'}</span>
2819
+
2820
+ /* block comment */
2821
+ </div>
2822
+ }`;
2823
+
2824
+ const result = await format(expected, { singleQuote: true });
2825
+ expect(result).toBeWithNewline(expected);
2826
+ });
2827
+
2828
+ it('should preserve a comment-only fragment body', async () => {
2829
+ const expected = `function App() @{
2830
+ <>
2831
+ /* only */
2832
+ </>
2833
+ }`;
2834
+
2835
+ const result = await format(expected, { singleQuote: true });
2836
+ expect(result).toBeWithNewline(expected);
2837
+ });
2838
+
2839
+ it('should keep blank lines around comments between template siblings', async () => {
2840
+ const expected = `function App() @{
2841
+ <>
2842
+ <ul></ul>
2843
+
2844
+ /* between */
2845
+
2846
+ <ul></ul>
2847
+ </>
2848
+ }`;
2849
+
2850
+ const result = await format(expected, { singleQuote: true });
2851
+ expect(result).toBeWithNewline(expected);
2852
+ });
2853
+
2728
2854
  it('should preserve trailing comments in function parameters', async () => {
2729
2855
  const expected = `function test(
2730
2856
  // comment in params