@tsrx/prettier-plugin 0.3.106 → 0.3.108

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.106",
3
+ "version": "0.3.108",
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.4"
28
28
  },
29
29
  "dependencies": {
30
- "@tsrx/core": "0.1.45"
30
+ "@tsrx/core": "0.1.46"
31
31
  },
32
32
  "files": [
33
33
  "src/"
package/src/index.js CHANGED
@@ -5937,6 +5937,70 @@ function printJSXTextChild(raw) {
5937
5937
  return join(hardline, lines);
5938
5938
  }
5939
5939
 
5940
+ /**
5941
+ * True when two consecutive printed JSX children stay glued on one line:
5942
+ * a text child directly adjacent (no whitespace) to its neighbor, as in
5943
+ * `{a}/{b}` or `<span>x</span>text`. Matches vanilla Prettier, which only
5944
+ * keeps siblings together across a whitespace-free text boundary — adjacent
5945
+ * expressions or elements (`{a}{b}`, `</p><p>`) still get their own lines.
5946
+ * @param {any} prevNode - Last source node of the previous printed child
5947
+ * @param {any} nextNode - First source node of the next printed child
5948
+ * @returns {boolean}
5949
+ */
5950
+ function isGluedJSXPair(prevNode, nextNode) {
5951
+ if (prevNode?.type !== 'JSXText' && nextNode?.type !== 'JSXText') {
5952
+ return false;
5953
+ }
5954
+ if (typeof prevNode?.end !== 'number' || prevNode.end !== nextNode?.start) {
5955
+ return false;
5956
+ }
5957
+ if (hasComment(prevNode) || hasComment(nextNode)) {
5958
+ return false;
5959
+ }
5960
+ if (prevNode.type === 'JSXText' && /\s$/u.test(prevNode.value)) {
5961
+ return false;
5962
+ }
5963
+ if (nextNode.type === 'JSXText' && /^\s/u.test(nextNode.value)) {
5964
+ return false;
5965
+ }
5966
+ return true;
5967
+ }
5968
+
5969
+ /**
5970
+ * Print a run of glued JSX children as one unit. Words inside text entries can
5971
+ * still wrap at their own spaces, but glued boundaries get no break opportunity.
5972
+ * @param {(Doc | string)[]} entries
5973
+ * @returns {Doc}
5974
+ */
5975
+ function printGluedJSXChildren(entries) {
5976
+ /** @type {Doc[]} */
5977
+ const parts = [];
5978
+ /** @type {Doc[]} */
5979
+ let current = [];
5980
+ const flush = () => {
5981
+ if (current.length > 0) {
5982
+ parts.push(current.length === 1 ? current[0] : current);
5983
+ current = [];
5984
+ }
5985
+ };
5986
+ for (const entry of entries) {
5987
+ if (typeof entry === 'string') {
5988
+ const words = entry.trim().split(/\s+/u);
5989
+ for (let i = 0; i < words.length; i++) {
5990
+ if (i > 0) {
5991
+ flush();
5992
+ parts.push(line);
5993
+ }
5994
+ current.push(words[i]);
5995
+ }
5996
+ } else {
5997
+ current.push(entry);
5998
+ }
5999
+ }
6000
+ flush();
6001
+ return parts.length === 1 ? parts[0] : fill(parts);
6002
+ }
6003
+
5940
6004
  /**
5941
6005
  * @param {string} raw
5942
6006
  * @returns {string}
@@ -6113,11 +6177,14 @@ function printJSXElement(node, path, options, print) {
6113
6177
 
6114
6178
  // Format children - filter out empty text nodes and merge adjacent text nodes.
6115
6179
  // childNodes tracks the source node behind each doc (a text run is a single
6116
- // JSXText) so the join can preserve authored blank lines.
6180
+ // JSXText) so the join can preserve authored blank lines; childEndNodes tracks
6181
+ // the last source node so glued neighbors can be detected by position.
6117
6182
  const childrenDocs = [];
6118
6183
  const childNodes = [];
6184
+ const childEndNodes = [];
6119
6185
  let currentText = '';
6120
6186
  let currentTextNode = null;
6187
+ let currentTextEndNode = null;
6121
6188
 
6122
6189
  for (let i = 0; i < node.children.length; i++) {
6123
6190
  const child = node.children[i];
@@ -6127,13 +6194,16 @@ function printJSXElement(node, path, options, print) {
6127
6194
  if (currentText) {
6128
6195
  childrenDocs.push(currentText);
6129
6196
  childNodes.push(currentTextNode);
6197
+ childEndNodes.push(currentTextEndNode);
6130
6198
  currentText = '';
6131
6199
  currentTextNode = null;
6200
+ currentTextEndNode = null;
6132
6201
  }
6133
6202
  const printedChild = path.call(print, 'children', i);
6134
6203
  if (printedChild !== '') {
6135
6204
  childrenDocs.push(printedChild);
6136
6205
  childNodes.push(child);
6206
+ childEndNodes.push(child);
6137
6207
  }
6138
6208
  continue;
6139
6209
  }
@@ -6152,11 +6222,14 @@ function printJSXElement(node, path, options, print) {
6152
6222
  if (currentText) {
6153
6223
  childrenDocs.push(currentText);
6154
6224
  childNodes.push(currentTextNode);
6225
+ childEndNodes.push(currentTextEndNode);
6155
6226
  currentText = '';
6156
6227
  currentTextNode = null;
6228
+ currentTextEndNode = null;
6157
6229
  }
6158
6230
  childrenDocs.push([text.trim(), ' ', path.call(print, 'children', i + 1), ';']);
6159
6231
  childNodes.push(child);
6232
+ childEndNodes.push(afterNextChild);
6160
6233
  i += 2;
6161
6234
  continue;
6162
6235
  }
@@ -6167,14 +6240,17 @@ function printJSXElement(node, path, options, print) {
6167
6240
  currentText = text;
6168
6241
  currentTextNode = child;
6169
6242
  }
6243
+ currentTextEndNode = child;
6170
6244
  }
6171
6245
  } else {
6172
6246
  // If we have accumulated text, push it before the non-text node
6173
6247
  if (currentText) {
6174
6248
  childrenDocs.push(currentText);
6175
6249
  childNodes.push(currentTextNode);
6250
+ childEndNodes.push(currentTextEndNode);
6176
6251
  currentText = '';
6177
6252
  currentTextNode = null;
6253
+ currentTextEndNode = null;
6178
6254
  }
6179
6255
 
6180
6256
  if (child.type === 'JSXExpressionContainer') {
@@ -6187,10 +6263,12 @@ function printJSXElement(node, path, options, print) {
6187
6263
  ...printTemplateChildTrailingComments(child),
6188
6264
  ]);
6189
6265
  childNodes.push(child);
6266
+ childEndNodes.push(child);
6190
6267
  } else {
6191
6268
  // Handle nested JSX elements
6192
6269
  childrenDocs.push(path.call(print, 'children', i));
6193
6270
  childNodes.push(child);
6271
+ childEndNodes.push(child);
6194
6272
  }
6195
6273
  }
6196
6274
  }
@@ -6199,6 +6277,7 @@ function printJSXElement(node, path, options, print) {
6199
6277
  if (currentText) {
6200
6278
  childrenDocs.push(currentText);
6201
6279
  childNodes.push(currentTextNode);
6280
+ childEndNodes.push(currentTextEndNode);
6202
6281
  }
6203
6282
 
6204
6283
  // A child with leading comments must break onto its own line, so the comment
@@ -6270,11 +6349,21 @@ function printJSXElement(node, path, options, print) {
6270
6349
  }
6271
6350
 
6272
6351
  // Multiple children or complex children - format with line breaks. Text runs
6273
- // fill/wrap to printWidth.
6352
+ // fill/wrap to printWidth. Children with no whitespace between them in the
6353
+ // source (`{a}/{b}`) stay glued as a single unit.
6274
6354
  const formattedChildren = [];
6275
6355
  for (let i = 0; i < childrenDocs.length; i++) {
6276
- const childDoc = childrenDocs[i];
6277
- formattedChildren.push(typeof childDoc === 'string' ? printRawText(childDoc) : childDoc);
6356
+ const unitEntries = [childrenDocs[i]];
6357
+ while (i < childrenDocs.length - 1 && isGluedJSXPair(childEndNodes[i], childNodes[i + 1])) {
6358
+ i++;
6359
+ unitEntries.push(childrenDocs[i]);
6360
+ }
6361
+ if (unitEntries.length === 1) {
6362
+ const childDoc = unitEntries[0];
6363
+ formattedChildren.push(typeof childDoc === 'string' ? printRawText(childDoc) : childDoc);
6364
+ } else {
6365
+ formattedChildren.push(printGluedJSXChildren(unitEntries));
6366
+ }
6278
6367
  if (i < childrenDocs.length - 1) {
6279
6368
  // Preserve a single authored blank line between children (2+ collapse to 1).
6280
6369
  const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
@@ -6391,10 +6480,19 @@ function printJSXFragment(node, path, options, print) {
6391
6480
  ]);
6392
6481
  }
6393
6482
 
6394
- // Multiple children or complex children - format with line breaks
6483
+ // Multiple children or complex children - format with line breaks. Children
6484
+ // with no whitespace between them in the source (`{a}/{b}`) stay glued as a
6485
+ // single unit.
6395
6486
  const formattedChildren = [];
6396
6487
  for (let i = 0; i < childrenDocs.length; i++) {
6397
- formattedChildren.push(childrenDocs[i]);
6488
+ const unitEntries = [childrenDocs[i]];
6489
+ while (i < childrenDocs.length - 1 && isGluedJSXPair(childNodes[i], childNodes[i + 1])) {
6490
+ i++;
6491
+ unitEntries.push(childrenDocs[i]);
6492
+ }
6493
+ formattedChildren.push(
6494
+ unitEntries.length === 1 ? unitEntries[0] : printGluedJSXChildren(unitEntries),
6495
+ );
6398
6496
  if (i < childrenDocs.length - 1) {
6399
6497
  // Preserve a single authored blank line between children (2+ collapse to 1).
6400
6498
  const blank = getBlankLinesBetweenNodes(childNodes[i], leadingAnchor(childNodes[i + 1])) > 0;
package/src/index.test.js CHANGED
@@ -501,6 +501,83 @@ const items=[1,2,3];
501
501
  expect(result).toBeWithNewline(expected);
502
502
  });
503
503
 
504
+ it('keeps expression children glued across whitespace-free text', async () => {
505
+ const input = `function Test() {
506
+ return <a href={x}>
507
+ {state.owner}/{state.repoName}
508
+ <ExternalLink className="w-3 h-3" />
509
+ </a>;
510
+ }`;
511
+ const expected = `function Test() {
512
+ return <a href={x}>
513
+ {state.owner}/{state.repoName}
514
+ <ExternalLink className="w-3 h-3" />
515
+ </a>;
516
+ }`;
517
+
518
+ const result = await format(input);
519
+ expect(result).toBeWithNewline(expected);
520
+ });
521
+
522
+ it('keeps expression siblings glued across multi-word text', async () => {
523
+ const input = `function Test() {
524
+ return <div>
525
+ {a}some words here{b}
526
+ <Foo />
527
+ </div>;
528
+ }`;
529
+ const expected = `function Test() {
530
+ return <div>
531
+ {a}some words here{b}
532
+ <Foo />
533
+ </div>;
534
+ }`;
535
+
536
+ const result = await format(input);
537
+ expect(result).toBeWithNewline(expected);
538
+ });
539
+
540
+ it('keeps whitespace-separated and directly adjacent expressions on their own lines', async () => {
541
+ const input = `function Test() {
542
+ return <div>
543
+ {a} / {b}
544
+ {c}{d}
545
+ <Foo />
546
+ </div>;
547
+ }`;
548
+ const expected = `function Test() {
549
+ return <div>
550
+ {a}
551
+ /
552
+ {b}
553
+ {c}
554
+ {d}
555
+ <Foo />
556
+ </div>;
557
+ }`;
558
+
559
+ const result = await format(input);
560
+ expect(result).toBeWithNewline(expected);
561
+ });
562
+
563
+ it('keeps fragment expression children glued across whitespace-free text', async () => {
564
+ const input = `function Test() {
565
+ return <>
566
+ {state.owner}/{state.repoName}
567
+ <Foo />
568
+ </>;
569
+ }`;
570
+ const expected = `function Test() {
571
+ return <>
572
+ {state.owner}/{state.repoName}
573
+ <Foo />
574
+ </>;
575
+ }`;
576
+
577
+ const result = await format(input);
578
+ expect(result).toBeWithNewline(expected);
579
+ });
580
+
504
581
  it('formats text line breaks properly', async () => {
505
582
  const input = `function Test() {
506
583
  return <div>