@tsrx/prettier-plugin 0.3.78 → 0.3.80

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.78",
3
+ "version": "0.3.80",
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.26"
30
+ "@tsrx/core": "0.1.28"
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[]}
@@ -5873,7 +5731,7 @@ function printJSXElement(node, path, options, print) {
5873
5731
  // Dynamic tags (`<{expr}>`) print the opening expression for both tags so
5874
5732
  // they stay textually identical; static names print as plain strings.
5875
5733
  const tagName =
5876
- /** @type {any} */ (openingElement.name).type === 'JSXExpressionContainer'
5734
+ openingElement.name.type === 'JSXExpressionContainer'
5877
5735
  ? ['{', path.call(print, 'openingElement', 'name', 'expression'), '}']
5878
5736
  : printJSXElementName(openingElement.name);
5879
5737
 
@@ -5960,26 +5818,13 @@ function printJSXElement(node, path, options, print) {
5960
5818
  { shouldBreak: shouldForceBreak },
5961
5819
  );
5962
5820
 
5963
- // Trailing comments after the last child are attached by the parser either to
5964
- // the closing tag (`closingElement.leadingComments`) or, when the last child is
5965
- // an `{expr}` container, to `metadata.elementLeadingComments` positioned inside
5966
- // the body (start >= opening tag end). Emit both before `</tag>`.
5967
- const openingTagEnd = /** @type {AST.NodeWithLocation} */ (openingElement).end;
5968
- const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
5969
- (/** @type {AST.Comment} */ comment) =>
5970
- 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,
5971
5826
  );
5972
- const trailingComments = [
5973
- ...(node.closingElement?.leadingComments ?? []),
5974
- ...bodyMetaComments,
5975
- ].sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
5976
- const lastMeaningfulChild = [...(node.children ?? [])]
5977
- .reverse()
5978
- .find((child) => child.type !== 'JSXText' || child.value.trim());
5979
- const closingCommentDocs = printElementBodyLineComments(trailingComments, lastMeaningfulChild);
5980
5827
  const hasClosingComments = closingCommentDocs.length > 0;
5981
- // A comment-only element has no children; its comments live in `innerComments`.
5982
- const innerCommentDocs = printElementBodyLineComments(node.innerComments);
5983
5828
 
5984
5829
  if (!hasChildren) {
5985
5830
  const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
@@ -6067,6 +5912,7 @@ function printJSXElement(node, path, options, print) {
6067
5912
  '{',
6068
5913
  path.call(print, 'children', i, 'expression'),
6069
5914
  '}',
5915
+ ...printTemplateChildTrailingComments(child),
6070
5916
  ]);
6071
5917
  childNodes.push(child);
6072
5918
  } else {
@@ -6112,7 +5958,7 @@ function printJSXElement(node, path, options, print) {
6112
5958
  ];
6113
5959
  }
6114
5960
  const meaningfulChildren = node.children.filter(
6115
- (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim(),
5961
+ (child) => child.type !== 'JSXText' || child.value.trim(),
6116
5962
  );
6117
5963
  const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
6118
5964
  if (
@@ -6127,9 +5973,9 @@ function printJSXElement(node, path, options, print) {
6127
5973
  !forceMultiline &&
6128
5974
  childrenDocs.length > 1 &&
6129
5975
  wasOriginallySingleLine(node) &&
6130
- node.children.some((/** @type {any} */ child) => child.type === 'JSXText') &&
5976
+ node.children.some((child) => child.type === 'JSXText') &&
6131
5977
  node.children.every(
6132
- (/** @type {any} */ child) =>
5978
+ (child) =>
6133
5979
  child.type === 'JSXText' || isSimpleJSXExpressionChild(/** @type {AST.Node} */ (child)),
6134
5980
  )
6135
5981
  ) {
@@ -6162,8 +6008,8 @@ function printJSXElement(node, path, options, print) {
6162
6008
 
6163
6009
  /**
6164
6010
  * Print a JSX fragment (<>...</>)
6165
- * @param {ESTreeJSX.JSXFragment} node - The JSX fragment node
6166
- * @param {AstPath<ESTreeJSX.JSXFragment>} path - The AST path
6011
+ * @param {AST.TSRXJSXFragment} node - The JSX fragment node
6012
+ * @param {AstPath<AST.TSRXJSXFragment>} path - The AST path
6167
6013
  * @param {RippleFormatOptions} options - Prettier options
6168
6014
  * @param {PrintFn} print - Print callback
6169
6015
  * @returns {Doc}
@@ -6171,12 +6017,23 @@ function printJSXElement(node, path, options, print) {
6171
6017
  function printJSXFragment(node, path, options, print) {
6172
6018
  const hasChildren = node.children && node.children.length > 0;
6173
6019
 
6020
+ // Comments before `</>` and the comments of a comment-only fragment.
6021
+ const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
6022
+ node,
6023
+ node.openingFragment,
6024
+ node.closingFragment,
6025
+ );
6026
+
6174
6027
  if (!hasChildren) {
6028
+ const bodyComments = [...innerCommentDocs, ...closingCommentDocs];
6029
+ if (bodyComments.length > 0) {
6030
+ return group(['<>', indent(bodyComments), hardline, '</>']);
6031
+ }
6175
6032
  return '<></>';
6176
6033
  }
6177
6034
 
6178
6035
  // A `@{ … }` code block is the whole body and hugs the tags: `<>@{ … }</>`.
6179
- if (node.children.length === 1 && /** @type {any} */ (node.children[0]).type === 'JSXCodeBlock') {
6036
+ if (node.children.length === 1 && node.children[0].type === 'JSXCodeBlock') {
6180
6037
  return group(['<>', path.call(print, 'children', 0), '</>']);
6181
6038
  }
6182
6039
 
@@ -6209,6 +6066,7 @@ function printJSXFragment(node, path, options, print) {
6209
6066
  '{',
6210
6067
  path.call(print, 'children', i, 'expression'),
6211
6068
  '}',
6069
+ ...printTemplateChildTrailingComments(child),
6212
6070
  ]);
6213
6071
  childNodes.push(child);
6214
6072
  } else {
@@ -6219,7 +6077,11 @@ function printJSXFragment(node, path, options, print) {
6219
6077
  }
6220
6078
 
6221
6079
  // Check if content can be inlined (single text node or single expression)
6222
- if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
6080
+ if (
6081
+ childrenDocs.length === 1 &&
6082
+ typeof childrenDocs[0] === 'string' &&
6083
+ closingCommentDocs.length === 0
6084
+ ) {
6223
6085
  return ['<>', childrenDocs[0], '</>'];
6224
6086
  }
6225
6087
  const meaningfulChildren = node.children.filter(
@@ -6230,6 +6092,7 @@ function printJSXFragment(node, path, options, print) {
6230
6092
  meaningfulChildren.length === 1 &&
6231
6093
  meaningfulChildren[0].type === 'JSXElement' &&
6232
6094
  wasOriginallySingleLine(node) &&
6095
+ closingCommentDocs.length === 0 &&
6233
6096
  !willBreak(childrenDocs[0])
6234
6097
  ) {
6235
6098
  // Keep the fragment inline when it fits; otherwise expand `<>` onto its own
@@ -6253,7 +6116,12 @@ function printJSXFragment(node, path, options, print) {
6253
6116
  }
6254
6117
 
6255
6118
  // Build the final fragment
6256
- return group(['<>', indent([hardline, ...formattedChildren]), hardline, '</>']);
6119
+ return group([
6120
+ '<>',
6121
+ indent([hardline, ...formattedChildren, ...closingCommentDocs]),
6122
+ hardline,
6123
+ '</>',
6124
+ ]);
6257
6125
  }
6258
6126
 
6259
6127
  /**
@@ -6262,14 +6130,14 @@ function printJSXFragment(node, path, options, print) {
6262
6130
  * opening tag's end, but the child is visited first). Pull those out of the
6263
6131
  * children and return a map from attribute index to the comments that precede it,
6264
6132
  * so the element printer can render them in the opening tag instead of the body.
6265
- * @param {AST.TSRXJSXElement} node
6133
+ * @param {AST.TSRXJSXElement | AST.JSXStyleElement} node
6266
6134
  * @returns {Map<number, AST.Comment[]>}
6267
6135
  */
6268
6136
  function collectOpeningTagComments(node) {
6269
6137
  /** @type {Map<number, AST.Comment[]>} */
6270
6138
  const byAttr = new Map();
6271
6139
  const openingElement = /** @type {AST.NodeWithLocation} */ (node.openingElement);
6272
- const attributes = /** @type {any[]} */ (node.openingElement?.attributes) ?? [];
6140
+ const attributes = node.openingElement?.attributes ?? [];
6273
6141
  if (!openingElement || attributes.length === 0 || !Array.isArray(node.children)) {
6274
6142
  return byAttr;
6275
6143
  }
@@ -6288,7 +6156,7 @@ function collectOpeningTagComments(node) {
6288
6156
  }
6289
6157
  }
6290
6158
  if (keep.length !== lead.length) {
6291
- /** @type {any} */ (child).leadingComments = keep;
6159
+ child.leadingComments = keep;
6292
6160
  }
6293
6161
  }
6294
6162
  if (collected.length === 0) return byAttr;
@@ -6338,18 +6206,74 @@ function printTemplateChildLeadingComments(child) {
6338
6206
  }
6339
6207
 
6340
6208
  /**
6341
- * Build doc parts for `//` line comments attached to an element body — trailing
6209
+ * Build doc parts for a template child's trailing comments (kept on the same
6210
+ * line as the child). Used for `{expr}` children, whose `{ … }` form is printed
6211
+ * inline by the JSX printers and so would otherwise skip the node's attached
6212
+ * trailing comments.
6213
+ * @param {AST.Node & AST.NodeWithMaybeComments} child
6214
+ * @returns {Doc[]}
6215
+ */
6216
+ function printTemplateChildTrailingComments(child) {
6217
+ const comments = child.trailingComments;
6218
+ if (!comments || comments.length === 0) {
6219
+ return [];
6220
+ }
6221
+ /** @type {Doc[]} */
6222
+ const parts = [];
6223
+ for (const comment of comments) {
6224
+ if (comment.type === 'Line') {
6225
+ parts.push(lineSuffix([' ', '//' + comment.value]));
6226
+ parts.push(breakParent);
6227
+ } else if (comment.type === 'Block') {
6228
+ parts.push(' /*' + comment.value + '*/');
6229
+ }
6230
+ }
6231
+ return parts;
6232
+ }
6233
+
6234
+ /**
6235
+ * Collect and print the comments that belong to an element/fragment body:
6236
+ * trailing comments after the last child (attached by the parser to the closing
6237
+ * tag's `leadingComments` or, when the last child is an `{expr}` container, to
6238
+ * `metadata.elementLeadingComments` positioned inside the body) and the comments
6239
+ * of a comment-only body (`innerComments`).
6240
+ * @param {AST.TSRXJSXElement | AST.TSRXJSXFragment} node
6241
+ * @param {AST.TSRXJSXElement['openingElement'] | AST.TSRXJSXFragment['openingFragment']} openingNode
6242
+ * @param {AST.TSRXJSXElement['closingElement'] | AST.TSRXJSXFragment['closingFragment']} closingNode
6243
+ * @returns {{ closingCommentDocs: Doc[], innerCommentDocs: Doc[] }}
6244
+ */
6245
+ function collectElementBodyCommentDocs(node, openingNode, closingNode) {
6246
+ const openingEnd = openingNode?.end;
6247
+ const bodyMetaComments = (node.metadata?.elementLeadingComments ?? []).filter(
6248
+ (/** @type {AST.Comment} */ comment) =>
6249
+ typeof comment.start === 'number' &&
6250
+ typeof openingEnd === 'number' &&
6251
+ comment.start >= openingEnd,
6252
+ );
6253
+ const trailingComments = [...(closingNode?.leadingComments ?? []), ...bodyMetaComments].sort(
6254
+ (/** @type {AST.Comment} */ a, /** @type {AST.Comment} */ b) =>
6255
+ /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
6256
+ );
6257
+ const lastMeaningfulChild = [...(node.children ?? [])]
6258
+ .reverse()
6259
+ .find((child) => child.type !== 'JSXText' || child.value.trim());
6260
+ return {
6261
+ closingCommentDocs: printElementBodyComments(trailingComments, lastMeaningfulChild),
6262
+ innerCommentDocs: printElementBodyComments(node.innerComments),
6263
+ };
6264
+ }
6265
+
6266
+ /**
6267
+ * Build doc parts for comments attached to an element body — trailing
6342
6268
  * comments before `</tag>` (`closingElement.leadingComments`) or the comments of a
6343
- * comment-only element (`innerComments`). Block comments are intentionally skipped:
6344
- * they survive in the adjacent JSXText value and are already rendered as text, so
6345
- * emitting them here would duplicate them. Each comment is emitted on its own line
6269
+ * comment-only element (`innerComments`). Each comment is emitted on its own line
6346
6270
  * at the children indent.
6347
6271
  * @param {AST.Comment[] | null | undefined} commentList
6348
6272
  * @param {any} [previousNode]
6349
6273
  * @returns {Doc[]}
6350
6274
  */
6351
- function printElementBodyLineComments(commentList, previousNode = null) {
6352
- const comments = (commentList ?? []).filter((comment) => comment.type === 'Line');
6275
+ function printElementBodyComments(commentList, previousNode = null) {
6276
+ const comments = commentList ?? [];
6353
6277
  if (comments.length === 0) {
6354
6278
  return [];
6355
6279
  }
@@ -6363,7 +6287,9 @@ function printElementBodyLineComments(commentList, previousNode = null) {
6363
6287
  if (prev && getBlankLinesBetweenNodes(prev, comments[i]) > 0) {
6364
6288
  parts.push(hardline);
6365
6289
  }
6366
- parts.push('//' + comments[i].value);
6290
+ parts.push(
6291
+ comments[i].type === 'Line' ? '//' + comments[i].value : '/*' + comments[i].value + '*/',
6292
+ );
6367
6293
  prev = comments[i];
6368
6294
  }
6369
6295
  return parts;
@@ -6404,10 +6330,10 @@ function printJSXCodeBlock(node, path, options, print) {
6404
6330
  parts.push(path.call(print, 'render'));
6405
6331
  }
6406
6332
  // Trailing comments after the last statement/render inside the block.
6407
- const innerCommentDocs = printElementBodyLineComments(node.innerComments);
6333
+ const innerCommentDocs = printElementBodyComments(node.innerComments);
6408
6334
  if (innerCommentDocs.length > 0) {
6409
6335
  const lastNode = node.render ?? node.body[node.body.length - 1];
6410
- const firstComment = (node.innerComments ?? []).find((c) => c.type === 'Line');
6336
+ const firstComment = (node.innerComments ?? [])[0];
6411
6337
  if (lastNode && firstComment && getBlankLinesBetweenNodes(lastNode, firstComment) > 0) {
6412
6338
  parts.push(hardline);
6413
6339
  }
@@ -6488,539 +6414,3 @@ function printJSXElementName(node) {
6488
6414
  }
6489
6415
  return 'Unknown';
6490
6416
  }
6491
-
6492
- /**
6493
- * Print a member expression as simple string (for element tag names)
6494
- * @param {AST.Node} node - The node to print
6495
- * @param {RippleFormatOptions} options - Prettier options
6496
- * @param {boolean} [computed=false] - Whether the property is computed
6497
- * @returns {string}
6498
- */
6499
- function printMemberExpressionSimple(node, options, computed = false) {
6500
- if (node.type === 'JSXIdentifier') {
6501
- return node.name;
6502
- }
6503
-
6504
- if (node.type === 'JSXMemberExpression') {
6505
- return (
6506
- printMemberExpressionSimple(node.object, options) +
6507
- '.' +
6508
- printMemberExpressionSimple(node.property, options, true)
6509
- );
6510
- }
6511
-
6512
- if (node.type === 'JSXNamespacedName') {
6513
- return node.namespace.name + ':' + node.name.name;
6514
- }
6515
-
6516
- if (node.type === 'Identifier') {
6517
- return node.name;
6518
- }
6519
-
6520
- if (node.type === 'MemberExpression') {
6521
- const obj = printMemberExpressionSimple(node.object, options);
6522
- let prop;
6523
- if (node.computed) {
6524
- prop = '[' + printMemberExpressionSimple(node.property, options, true) + ']';
6525
- } else {
6526
- prop = '.' + printMemberExpressionSimple(node.property, options, true);
6527
- }
6528
- return obj + prop;
6529
- }
6530
-
6531
- if (node.type === 'Literal') {
6532
- return computed ? formatStringLiteral(node.value, options) : JSON.stringify(node.value);
6533
- }
6534
- return '';
6535
- }
6536
-
6537
- /**
6538
- * Check whether an attribute value can expand into multiline content.
6539
- * @param {AST.Expression | null | undefined} value - The attribute value node
6540
- * @param {boolean} [is_nested_in_object=false] - Whether this value is nested within an object literal
6541
- * @returns {boolean}
6542
- */
6543
- function is_attribute_value_breakable(value, is_nested_in_object = false) {
6544
- if (!value) return false;
6545
-
6546
- switch (value.type) {
6547
- case 'ConditionalExpression':
6548
- // Keep simple top-level ternary attributes inline when they fit.
6549
- // We only force-break when a conditional is nested in an object literal value.
6550
- return is_nested_in_object;
6551
- case 'ObjectExpression':
6552
- return value.properties.some(
6553
- (property) =>
6554
- property.type === 'Property' &&
6555
- is_attribute_value_breakable(/** @type {AST.Expression} */ (property.value), true),
6556
- );
6557
- default:
6558
- return false;
6559
- }
6560
- }
6561
-
6562
- /**
6563
- * Print a JSX element node
6564
- * @param {AST.Element} element - The element node
6565
- * @param {AstPath<AST.Element>} path - The AST path
6566
- * @param {RippleFormatOptions} options - Prettier options
6567
- * @param {PrintFn} print - Print callback
6568
- * @returns {Doc}
6569
- */
6570
- function printElement(element, path, options, print) {
6571
- const node = /** @type {any} */ (element);
6572
- const tagName = printMemberExpressionSimple(node.id, options);
6573
- const openingElement = /** @type {any} */ (node.openingElement);
6574
- /** @type {Doc} */
6575
- let typeArgsDoc = '';
6576
- if (openingElement?.typeArguments) {
6577
- typeArgsDoc = path.call(print, 'openingElement', 'typeArguments');
6578
- }
6579
- const elementLeadingComments = getElementLeadingComments(/** @type {any} */ (node));
6580
-
6581
- // `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
6582
- // body (after the opening tag). Those must not be hoisted before the element.
6583
- const outerElementLeadingComments = elementLeadingComments.filter(
6584
- (/** @type {AST.Comment} */ comment) =>
6585
- typeof comment.start !== 'number' || comment.start < node.start,
6586
- );
6587
- const innerElementBodyComments = elementLeadingComments.filter(
6588
- (/** @type {AST.Comment} */ comment) =>
6589
- typeof comment.start === 'number' &&
6590
- comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
6591
- comment.start < node.end,
6592
- );
6593
- const metadataCommentParts =
6594
- outerElementLeadingComments.length > 0
6595
- ? createElementLevelCommentParts(outerElementLeadingComments)
6596
- : [];
6597
- const fallbackElementComments = [];
6598
- const shouldLiftTextLevelComments = outerElementLeadingComments.length === 0;
6599
-
6600
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
6601
- const hasInnerComments = Array.isArray(node.innerComments) && node.innerComments.length > 0;
6602
- const isSelfClosing = !!node.selfClosing;
6603
- const hasAttributes = Array.isArray(node.attributes) && node.attributes.length > 0;
6604
-
6605
- // Collect comments that the parser attached to children but actually belong inside
6606
- // the opening tag (positionally before openingElement.end). These should be printed
6607
- // as leading comments before the appropriate attribute, not lifted to element-level.
6608
- /** @type {Set<AST.Comment>} */
6609
- const openingTagCommentsSet = new Set();
6610
- if (hasChildren && node.openingElement) {
6611
- const openingEnd = /** @type {AST.NodeWithLocation} */ (node.openingElement).end;
6612
- for (const child of node.children) {
6613
- if (
6614
- (child.type === 'JSXExpressionContainer' || child.type === 'JSXText') &&
6615
- Array.isArray(child.leadingComments)
6616
- ) {
6617
- for (const comment of child.leadingComments) {
6618
- if (
6619
- typeof comment.start === 'number' &&
6620
- comment.start >= node.start &&
6621
- comment.start < openingEnd
6622
- ) {
6623
- openingTagCommentsSet.add(comment);
6624
- }
6625
- }
6626
- }
6627
- }
6628
- }
6629
-
6630
- // Build a map from attribute index to comments that should precede that attribute
6631
- const openingTagCommentsByAttrIndex = new Map();
6632
- if (openingTagCommentsSet.size > 0 && hasAttributes) {
6633
- const sortedOTC = [...openingTagCommentsSet].sort(
6634
- (a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
6635
- );
6636
- let commentIdx = 0;
6637
- for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++) {
6638
- const attr = node.attributes[attrIdx];
6639
- const commentsForAttr = [];
6640
- while (
6641
- commentIdx < sortedOTC.length &&
6642
- /** @type {number} */ (sortedOTC[commentIdx].start) <
6643
- /** @type {AST.NodeWithLocation} */ (attr).start
6644
- ) {
6645
- commentsForAttr.push(sortedOTC[commentIdx]);
6646
- commentIdx++;
6647
- }
6648
- if (commentsForAttr.length > 0) {
6649
- openingTagCommentsByAttrIndex.set(attrIdx, commentsForAttr);
6650
- }
6651
- }
6652
- }
6653
-
6654
- if (isSelfClosing && !hasInnerComments && !hasAttributes) {
6655
- const elementDoc = group(['<', tagName, typeArgsDoc, ' />']);
6656
- return metadataCommentParts.length > 0 ? [...metadataCommentParts, elementDoc] : elementDoc;
6657
- }
6658
-
6659
- // Determine the line break type for attributes
6660
- // When singleAttributePerLine is true, force each attribute on its own line with hardline
6661
- // Otherwise, use line to allow collapsing when it fits
6662
- const attrLineBreak = options.singleAttributePerLine ? hardline : line;
6663
-
6664
- const shouldUseSelfClosingSyntax = isSelfClosing || (!hasChildren && !hasInnerComments);
6665
-
6666
- const hasOpeningTagComments = openingTagCommentsSet.size > 0;
6667
- let attrIndex = 0;
6668
- let hasBreakingAttribute = false;
6669
- const attrDocs = hasAttributes
6670
- ? path.map((attrPath) => {
6671
- const idx = attrIndex++;
6672
- const commentsForAttr = openingTagCommentsByAttrIndex.get(idx);
6673
- /** @type {Doc[]} */
6674
- const parts = [];
6675
- if (commentsForAttr) {
6676
- for (const comment of commentsForAttr) {
6677
- // Line comments (//) consume the rest of the line, so they must
6678
- // use hardline to force a break. Block comments can use normal breaks.
6679
- if (comment.type === 'Line') {
6680
- parts.push(hardline);
6681
- parts.push('//' + comment.value);
6682
- } else if (comment.type === 'Block') {
6683
- parts.push(attrLineBreak);
6684
- parts.push('/*' + comment.value + '*/');
6685
- }
6686
- }
6687
- }
6688
- parts.push(attrLineBreak);
6689
- const attrDoc = print(attrPath);
6690
- parts.push(attrDoc);
6691
- const attr_node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
6692
- /** @type {unknown} */ (attrPath.node)
6693
- );
6694
- if (
6695
- !hasBreakingAttribute &&
6696
- (willBreak(attrDoc) ||
6697
- (attr_node.type === 'JSXAttribute' &&
6698
- is_attribute_value_breakable(/** @type {any} */ (attr_node.value))))
6699
- ) {
6700
- hasBreakingAttribute = true;
6701
- }
6702
- return parts;
6703
- }, 'attributes')
6704
- : [];
6705
- const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
6706
- const openingTagAlwaysBreaks =
6707
- (hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
6708
- const openingTag = group([
6709
- '<',
6710
- tagName,
6711
- typeArgsDoc,
6712
- hasAttributes
6713
- ? indent([
6714
- ...attrDocs,
6715
- // Force the group to break when there are line comments in the opening tag,
6716
- // or when any attribute value would break (e.g. multiline objects, ternaries).
6717
- // This ensures attributes are broken onto separate lines rather than breaking
6718
- // expression values inline on the same line as the tag name.
6719
- ...(shouldForceBreak ? [breakParent] : []),
6720
- ])
6721
- : '',
6722
- // Add line break opportunity before > or />
6723
- // Use line for self-closing (keeps space), softline for non-self-closing when attributes present
6724
- // When bracketSameLine is true, don't add line break for non-self-closing elements
6725
- shouldUseSelfClosingSyntax
6726
- ? hasAttributes
6727
- ? line
6728
- : ''
6729
- : hasAttributes && !options.bracketSameLine
6730
- ? softline
6731
- : '',
6732
- shouldUseSelfClosingSyntax ? (hasAttributes ? '/>' : ' />') : '>',
6733
- ]);
6734
-
6735
- if (!hasChildren) {
6736
- if (!hasInnerComments) {
6737
- return metadataCommentParts.length > 0 ? [...metadataCommentParts, openingTag] : openingTag;
6738
- }
6739
-
6740
- /** @type {Doc[]} */
6741
- const innerParts = [];
6742
- for (const comment of node.innerComments ?? []) {
6743
- if (comment.type === 'Line') {
6744
- innerParts.push('//' + comment.value);
6745
- innerParts.push(hardline);
6746
- } else if (comment.type === 'Block') {
6747
- innerParts.push('/*' + comment.value + '*/');
6748
- innerParts.push(hardline);
6749
- }
6750
- }
6751
-
6752
- if (innerParts.length > 0 && innerParts[innerParts.length - 1] === hardline) {
6753
- innerParts.pop();
6754
- }
6755
-
6756
- const closingTag = ['</', tagName, '>'];
6757
- const elementOutput = group([
6758
- openingTag,
6759
- indent([hardline, ...innerParts]),
6760
- hardline,
6761
- closingTag,
6762
- ]);
6763
- return metadataCommentParts.length > 0
6764
- ? [...metadataCommentParts, elementOutput]
6765
- : elementOutput;
6766
- }
6767
-
6768
- // Has children - use unified children processing
6769
- // Build children with whitespace preservation
6770
- /** @type {Doc[]} */
6771
- const finalChildren = [];
6772
- const sortedInnerElementBodyComments =
6773
- innerElementBodyComments.length > 0
6774
- ? innerElementBodyComments.slice().sort((a, b) => (a.start ?? 0) - (b.start ?? 0))
6775
- : [];
6776
- let innerElementBodyCommentIndex = 0;
6777
-
6778
- for (let i = 0; i < node.children.length; i++) {
6779
- const currentChild = node.children[i];
6780
- const nextChild = node.children[i + 1];
6781
-
6782
- // Insert any element-body comments that appear before this child.
6783
- if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6784
- const currentChildStart = typeof currentChild.start === 'number' ? currentChild.start : null;
6785
- if (currentChildStart != null) {
6786
- const commentsBefore = [];
6787
- while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6788
- /** @type {AST.Comment} */
6789
- const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
6790
- if (typeof comment.start !== 'number' || comment.start >= currentChildStart) {
6791
- break;
6792
- }
6793
- commentsBefore.push(comment);
6794
- innerElementBodyCommentIndex++;
6795
- }
6796
- if (commentsBefore.length > 0) {
6797
- if (finalChildren.length > 0) {
6798
- finalChildren.push(hardline);
6799
- }
6800
- finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBefore));
6801
- finalChildren.push(hardline);
6802
- }
6803
- }
6804
- }
6805
-
6806
- const isTextLikeChild =
6807
- currentChild.type === 'JSXExpressionContainer' || currentChild.type === 'JSXText';
6808
- const hasTextLeadingComments =
6809
- shouldLiftTextLevelComments &&
6810
- isTextLikeChild &&
6811
- Array.isArray(currentChild.leadingComments) &&
6812
- currentChild.leadingComments.length > 0;
6813
- const currentChildAny = /** @type {any} */ (currentChild);
6814
- const rawExpressionLeadingComments =
6815
- isTextLikeChild && Array.isArray(currentChildAny.expression?.leadingComments)
6816
- ? currentChildAny.expression.leadingComments
6817
- : null;
6818
- const elementBodyLeadingComments =
6819
- hasTextLeadingComments && node.openingElement
6820
- ? /** @type {AST.Comment[]} */ (currentChild.leadingComments).filter(
6821
- (comment) =>
6822
- comment.context?.containerId === node.metadata?.commentContainerId &&
6823
- comment.context?.beforeMeaningfulChild &&
6824
- typeof comment.start === 'number' &&
6825
- comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
6826
- comment.start < /** @type {AST.NodeWithLocation} */ (currentChild).start,
6827
- )
6828
- : [];
6829
-
6830
- if (hasTextLeadingComments) {
6831
- for (let j = 0; j < /** @type {AST.Comment[]} */ (currentChild.leadingComments).length; j++) {
6832
- const comment = /** @type {AST.Comment[]} */ (currentChild.leadingComments)[j];
6833
- // Don't lift comments that belong inside the opening tag (handled in attribute section)
6834
- if (!openingTagCommentsSet.has(comment) && !elementBodyLeadingComments.includes(comment)) {
6835
- fallbackElementComments.push(comment);
6836
- }
6837
- }
6838
- }
6839
-
6840
- const childPrintArgs = /** @type {PrintArgs} */ ({});
6841
- if (hasTextLeadingComments) {
6842
- childPrintArgs.suppressLeadingComments = true;
6843
- }
6844
- if (rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0) {
6845
- childPrintArgs.suppressExpressionLeadingComments = true;
6846
- }
6847
-
6848
- const printedChild =
6849
- Object.keys(childPrintArgs).length > 0
6850
- ? path.call((childPath) => print(childPath, childPrintArgs), 'children', i)
6851
- : path.call(print, 'children', i);
6852
-
6853
- const childLeadingCommentParts =
6854
- elementBodyLeadingComments.length > 0
6855
- ? createElementLevelCommentParts(elementBodyLeadingComments)
6856
- : rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
6857
- ? createElementLevelCommentParts(rawExpressionLeadingComments)
6858
- : null;
6859
- const childDoc = childLeadingCommentParts
6860
- ? [...childLeadingCommentParts, printedChild]
6861
- : printedChild;
6862
- finalChildren.push(childDoc);
6863
-
6864
- // Insert element-body comments that fall between this child and the next child (or the closing tag).
6865
- let insertedBodyCommentsBetween = false;
6866
- if (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6867
- const currentChildEnd = /** @type {AST.NodeWithLocation} */ (currentChild).end;
6868
- const nextChildStart =
6869
- nextChild && typeof nextChild.start === 'number' ? nextChild.start : null;
6870
- const commentsBetween = [];
6871
- while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6872
- /** @type {AST.Comment} */
6873
- const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
6874
- if (typeof comment.start !== 'number') {
6875
- innerElementBodyCommentIndex++;
6876
- continue;
6877
- }
6878
- if (comment.start < currentChildEnd) {
6879
- break;
6880
- }
6881
- if (nextChildStart != null && comment.start >= nextChildStart) {
6882
- break;
6883
- }
6884
- commentsBetween.push(comment);
6885
- innerElementBodyCommentIndex++;
6886
- }
6887
- if (commentsBetween.length > 0) {
6888
- const firstComment = commentsBetween[0];
6889
- const lastComment = commentsBetween[commentsBetween.length - 1];
6890
-
6891
- // Preserve any blank line(s) that existed between the previous child and the comment block.
6892
- const blankLinesBefore = getBlankLinesBetweenNodes(currentChild, firstComment);
6893
- finalChildren.push(hardline);
6894
- if (blankLinesBefore > 0) {
6895
- finalChildren.push(hardline);
6896
- }
6897
-
6898
- finalChildren.push(...createElementLevelCommentPartsTrimmed(commentsBetween));
6899
-
6900
- if (nextChild) {
6901
- // Preserve any blank line(s) that existed between the comment block and the next child.
6902
- const blankLinesAfter = getBlankLinesBetweenNodes(lastComment, nextChild);
6903
- finalChildren.push(hardline);
6904
- if (blankLinesAfter > 0) {
6905
- finalChildren.push(hardline);
6906
- }
6907
- }
6908
- insertedBodyCommentsBetween = true;
6909
- }
6910
- }
6911
-
6912
- if (nextChild) {
6913
- if (insertedBodyCommentsBetween) {
6914
- continue;
6915
- }
6916
- const whitespaceTarget =
6917
- nextChild.leadingComments && nextChild.leadingComments.length > 0
6918
- ? nextChild.leadingComments[0]
6919
- : nextChild;
6920
- const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
6921
- const isTextOrExpressionChild =
6922
- currentChild.type === 'JSXExpressionContainer' ||
6923
- currentChild.type === 'JSXText' ||
6924
- nextChild.type === 'JSXExpressionContainer' ||
6925
- nextChild.type === 'JSXText';
6926
-
6927
- if (whitespaceLinesCount > 0) {
6928
- finalChildren.push(hardline);
6929
- finalChildren.push(hardline);
6930
- } else if (!isTextOrExpressionChild && shouldAddBlankLine(currentChild, nextChild)) {
6931
- finalChildren.push(hardline);
6932
- finalChildren.push(hardline);
6933
- } else {
6934
- finalChildren.push(hardline);
6935
- }
6936
- }
6937
- }
6938
-
6939
- // Collect comments attached to the closing element (comments that appear after the last child
6940
- // but before the closing tag, e.g. `</div>`). The parser attaches these as leadingComments
6941
- // on either the closingElement node or the closingElement.name node depending on context.
6942
- const closingElementComments = [
6943
- ...(node.closingElement && Array.isArray(node.closingElement.leadingComments)
6944
- ? node.closingElement.leadingComments
6945
- : []),
6946
- ...(node.closingElement &&
6947
- node.closingElement.name &&
6948
- Array.isArray(node.closingElement.name.leadingComments)
6949
- ? node.closingElement.name.leadingComments
6950
- : []),
6951
- ];
6952
-
6953
- if (closingElementComments.length > 0) {
6954
- const lastChild = node.children[node.children.length - 1];
6955
- if (lastChild) {
6956
- const blankLinesBefore = getBlankLinesBetweenNodes(lastChild, closingElementComments[0]);
6957
- finalChildren.push(hardline);
6958
- if (blankLinesBefore > 0) {
6959
- finalChildren.push(hardline);
6960
- }
6961
- }
6962
- finalChildren.push(...createElementLevelCommentPartsTrimmed(closingElementComments));
6963
- }
6964
-
6965
- const fallbackCommentParts =
6966
- fallbackElementComments.length > 0
6967
- ? createElementLevelCommentParts(fallbackElementComments)
6968
- : [];
6969
- const leadingCommentParts =
6970
- metadataCommentParts.length > 0
6971
- ? [...metadataCommentParts, ...fallbackCommentParts]
6972
- : fallbackCommentParts;
6973
-
6974
- const closingTag = ['</', tagName, '>'];
6975
- let elementOutput;
6976
- const shouldTryInlineMultipleChildren =
6977
- !openingTagAlwaysBreaks &&
6978
- fallbackCommentParts.length === 0 &&
6979
- closingElementComments.length === 0 &&
6980
- shouldTryInlineMultipleTextChildren(node);
6981
-
6982
- if (finalChildren.length === 1) {
6983
- const child = finalChildren[0];
6984
- const firstChild = /** @type {any} */ (node.children[0]);
6985
- const isNonSelfClosingElement =
6986
- firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6987
- const isElementChild = firstChild && firstChild.type === 'Element';
6988
- const isRawTextChild =
6989
- firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
6990
-
6991
- if (
6992
- (typeof child === 'string' || isRawTextChild) &&
6993
- shouldInlineSingleChild(node, firstChild, child)
6994
- ) {
6995
- elementOutput = openingTagAlwaysBreaks
6996
- ? [openingTag, indent([hardline, child]), hardline, closingTag]
6997
- : conditionalGroup([
6998
- group([openingTag, child, closingTag]),
6999
- [openingTag, indent([hardline, child]), hardline, closingTag],
7000
- ]);
7001
- } else if (
7002
- child &&
7003
- typeof child === 'object' &&
7004
- !isNonSelfClosingElement &&
7005
- shouldInlineSingleChild(node, firstChild, child)
7006
- ) {
7007
- if (isElementChild && hasAttributes) {
7008
- elementOutput = [openingTag, indent([hardline, child]), hardline, closingTag];
7009
- } else {
7010
- elementOutput = group([openingTag, indent([softline, child]), softline, closingTag]);
7011
- }
7012
- } else {
7013
- elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
7014
- }
7015
- } else if (shouldTryInlineMultipleChildren) {
7016
- const inlineChildren = path.map(print, 'children');
7017
- elementOutput = conditionalGroup([
7018
- group([openingTag, ...inlineChildren, closingTag]),
7019
- [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag],
7020
- ]);
7021
- } else {
7022
- elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
7023
- }
7024
-
7025
- return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
7026
- }
package/src/index.test.js CHANGED
@@ -2753,6 +2753,128 @@ function test() {
2753
2753
  expect(result).toBeWithNewline(expected);
2754
2754
  });
2755
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
+
2854
+ it('should keep a trailing line comment after an expression container child', async () => {
2855
+ const expected = `function App() @{
2856
+ <>
2857
+ {q} // hey
2858
+ // hello
2859
+ </>
2860
+ }`;
2861
+
2862
+ const result = await format(expected, { singleQuote: true });
2863
+ expect(result).toBeWithNewline(expected);
2864
+ });
2865
+
2866
+ it('should keep a trailing block comment after an expression container child', async () => {
2867
+ const expected = `function App() {
2868
+ <div>
2869
+ {x} /* note */
2870
+ <span>{'tail'}</span>
2871
+ </div>
2872
+ }`;
2873
+
2874
+ const result = await format(expected, { singleQuote: true });
2875
+ expect(result).toBeWithNewline(expected);
2876
+ });
2877
+
2756
2878
  it('should preserve trailing comments in function parameters', async () => {
2757
2879
  const expected = `function test(
2758
2880
  // comment in params