@tsrx/prettier-plugin 0.3.107 → 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 +1 -1
- package/src/index.js +104 -6
- package/src/index.test.js +77 -0
package/package.json
CHANGED
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
|
|
6277
|
-
|
|
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
|
-
|
|
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>
|