@tsrx/prettier-plugin 0.3.58 → 0.3.60
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 +2 -2
- package/src/index.js +76 -30
- package/src/index.test.js +182 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsrx/prettier-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.60",
|
|
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.
|
|
30
|
+
"@tsrx/core": "0.1.10"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src/"
|
package/src/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const {
|
|
|
36
36
|
indentIfBreak,
|
|
37
37
|
lineSuffix,
|
|
38
38
|
} = builders;
|
|
39
|
-
const { willBreak } = utils;
|
|
39
|
+
const { replaceEndOfLine, willBreak } = utils;
|
|
40
40
|
|
|
41
41
|
/** @type {import('prettier').Plugin['languages']} */
|
|
42
42
|
export const languages = [
|
|
@@ -2236,7 +2236,7 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
2236
2236
|
|
|
2237
2237
|
case 'Text': {
|
|
2238
2238
|
if (typeof node.raw === 'string') {
|
|
2239
|
-
nodeContent = node.raw;
|
|
2239
|
+
nodeContent = printRawText(node.raw);
|
|
2240
2240
|
break;
|
|
2241
2241
|
}
|
|
2242
2242
|
|
|
@@ -4286,23 +4286,17 @@ function printTSInterfaceBody(node, path, options, print) {
|
|
|
4286
4286
|
* @param {AstPath<AST.TSTypeAliasDeclaration>} path - The AST path
|
|
4287
4287
|
* @param {RippleFormatOptions} options - Prettier options
|
|
4288
4288
|
* @param {PrintFn} print - Print callback
|
|
4289
|
-
* @returns {Doc
|
|
4289
|
+
* @returns {Doc}
|
|
4290
4290
|
*/
|
|
4291
4291
|
function printTSTypeAliasDeclaration(node, path, options, print) {
|
|
4292
4292
|
/** @type {Doc[]} */
|
|
4293
|
-
const
|
|
4294
|
-
parts.push('type ');
|
|
4295
|
-
parts.push(node.id.name);
|
|
4293
|
+
const head = ['type ', node.id.name];
|
|
4296
4294
|
|
|
4297
4295
|
if (node.typeParameters) {
|
|
4298
|
-
|
|
4296
|
+
head.push(path.call(print, 'typeParameters'));
|
|
4299
4297
|
}
|
|
4300
4298
|
|
|
4301
|
-
|
|
4302
|
-
parts.push(path.call(print, 'typeAnnotation'));
|
|
4303
|
-
parts.push(semi(options));
|
|
4304
|
-
|
|
4305
|
-
return parts;
|
|
4299
|
+
return group([head, ' =', indent([line, path.call(print, 'typeAnnotation')]), semi(options)]);
|
|
4306
4300
|
}
|
|
4307
4301
|
|
|
4308
4302
|
/**
|
|
@@ -5394,19 +5388,22 @@ function printTSConstructorType(node, path, options, print) {
|
|
|
5394
5388
|
* @param {AstPath<AST.TSConditionalType>} path - The AST path
|
|
5395
5389
|
* @param {RippleFormatOptions} options - Prettier options
|
|
5396
5390
|
* @param {PrintFn} print - Print callback
|
|
5397
|
-
* @returns {Doc
|
|
5391
|
+
* @returns {Doc}
|
|
5398
5392
|
*/
|
|
5399
5393
|
function printTSConditionalType(node, path, options, print) {
|
|
5400
|
-
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5394
|
+
const trueType = path.call(print, 'trueType');
|
|
5395
|
+
const falseType = path.call(print, 'falseType');
|
|
5396
|
+
|
|
5397
|
+
const shouldIndentTrueType = node.trueType.type !== 'TSConditionalType';
|
|
5398
|
+
const shouldIndentFalseType = node.falseType.type !== 'TSConditionalType';
|
|
5399
|
+
|
|
5400
|
+
return group([
|
|
5401
|
+
path.call(print, 'checkType'),
|
|
5402
|
+
' extends ',
|
|
5403
|
+
path.call(print, 'extendsType'),
|
|
5404
|
+
indent([line, '? ', shouldIndentTrueType ? indent(trueType) : trueType]),
|
|
5405
|
+
indent([line, ': ', shouldIndentFalseType ? indent(falseType) : falseType]),
|
|
5406
|
+
]);
|
|
5410
5407
|
}
|
|
5411
5408
|
|
|
5412
5409
|
/**
|
|
@@ -5504,6 +5501,27 @@ function printTSIndexedAccessType(node, path, options, print) {
|
|
|
5504
5501
|
return [path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']'];
|
|
5505
5502
|
}
|
|
5506
5503
|
|
|
5504
|
+
/**
|
|
5505
|
+
* Print direct TSRX text so it can wrap like JSX text when an element body breaks.
|
|
5506
|
+
* @param {string} raw
|
|
5507
|
+
* @returns {Doc}
|
|
5508
|
+
*/
|
|
5509
|
+
function printRawText(raw) {
|
|
5510
|
+
const text = raw.trim().replace(/(?:\r\n|\r|\n)[^\S\r\n]+/gu, ' ');
|
|
5511
|
+
if (!text) {
|
|
5512
|
+
return '';
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
return fill(
|
|
5516
|
+
text
|
|
5517
|
+
.split(/([^\S\r\n]+)/u)
|
|
5518
|
+
.filter(Boolean)
|
|
5519
|
+
.map((part) => {
|
|
5520
|
+
return /^[^\S\r\n]+$/u.test(part) ? line : replaceEndOfLine(part);
|
|
5521
|
+
}),
|
|
5522
|
+
);
|
|
5523
|
+
}
|
|
5524
|
+
|
|
5507
5525
|
/**
|
|
5508
5526
|
* @param {AST.Node} parentNode
|
|
5509
5527
|
* @param {AST.Node} firstChild
|
|
@@ -6273,6 +6291,8 @@ function printElement(element, path, options, print) {
|
|
|
6273
6291
|
}, 'attributes')
|
|
6274
6292
|
: [];
|
|
6275
6293
|
const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
|
|
6294
|
+
const openingTagAlwaysBreaks =
|
|
6295
|
+
(hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
|
|
6276
6296
|
const openingTag = group([
|
|
6277
6297
|
'<',
|
|
6278
6298
|
tagName,
|
|
@@ -6384,12 +6404,23 @@ function printElement(element, path, options, print) {
|
|
|
6384
6404
|
isTextLikeChild && Array.isArray(currentChild.expression?.leadingComments)
|
|
6385
6405
|
? currentChild.expression.leadingComments
|
|
6386
6406
|
: null;
|
|
6407
|
+
const elementBodyLeadingComments =
|
|
6408
|
+
hasTextLeadingComments && node.openingElement
|
|
6409
|
+
? /** @type {AST.Comment[]} */ (currentChild.leadingComments).filter(
|
|
6410
|
+
(comment) =>
|
|
6411
|
+
comment.context?.containerId === node.metadata?.commentContainerId &&
|
|
6412
|
+
comment.context?.beforeMeaningfulChild &&
|
|
6413
|
+
typeof comment.start === 'number' &&
|
|
6414
|
+
comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
|
|
6415
|
+
comment.start < /** @type {AST.NodeWithLocation} */ (currentChild).start,
|
|
6416
|
+
)
|
|
6417
|
+
: [];
|
|
6387
6418
|
|
|
6388
6419
|
if (hasTextLeadingComments) {
|
|
6389
6420
|
for (let j = 0; j < /** @type {AST.Comment[]} */ (currentChild.leadingComments).length; j++) {
|
|
6390
6421
|
const comment = /** @type {AST.Comment[]} */ (currentChild.leadingComments)[j];
|
|
6391
6422
|
// Don't lift comments that belong inside the opening tag (handled in attribute section)
|
|
6392
|
-
if (!openingTagCommentsSet.has(comment)) {
|
|
6423
|
+
if (!openingTagCommentsSet.has(comment) && !elementBodyLeadingComments.includes(comment)) {
|
|
6393
6424
|
fallbackElementComments.push(comment);
|
|
6394
6425
|
}
|
|
6395
6426
|
}
|
|
@@ -6408,10 +6439,15 @@ function printElement(element, path, options, print) {
|
|
|
6408
6439
|
? path.call((childPath) => print(childPath, childPrintArgs), 'children', i)
|
|
6409
6440
|
: path.call(print, 'children', i);
|
|
6410
6441
|
|
|
6411
|
-
const
|
|
6412
|
-
|
|
6413
|
-
?
|
|
6414
|
-
:
|
|
6442
|
+
const childLeadingCommentParts =
|
|
6443
|
+
elementBodyLeadingComments.length > 0
|
|
6444
|
+
? createElementLevelCommentParts(elementBodyLeadingComments)
|
|
6445
|
+
: rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
|
|
6446
|
+
? createElementLevelCommentParts(rawExpressionLeadingComments)
|
|
6447
|
+
: null;
|
|
6448
|
+
const childDoc = childLeadingCommentParts
|
|
6449
|
+
? [...childLeadingCommentParts, printedChild]
|
|
6450
|
+
: printedChild;
|
|
6415
6451
|
finalChildren.push(childDoc);
|
|
6416
6452
|
|
|
6417
6453
|
// Insert element-body comments that fall between this child and the next child (or the closing tag).
|
|
@@ -6538,9 +6574,19 @@ function printElement(element, path, options, print) {
|
|
|
6538
6574
|
const isNonSelfClosingElement =
|
|
6539
6575
|
firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
|
|
6540
6576
|
const isElementChild = firstChild && firstChild.type === 'Element';
|
|
6577
|
+
const isRawTextChild =
|
|
6578
|
+
firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
|
|
6541
6579
|
|
|
6542
|
-
if (
|
|
6543
|
-
|
|
6580
|
+
if (
|
|
6581
|
+
(typeof child === 'string' || isRawTextChild) &&
|
|
6582
|
+
shouldInlineSingleChild(node, firstChild, child)
|
|
6583
|
+
) {
|
|
6584
|
+
elementOutput = openingTagAlwaysBreaks
|
|
6585
|
+
? [openingTag, indent([hardline, child]), hardline, closingTag]
|
|
6586
|
+
: conditionalGroup([
|
|
6587
|
+
group([openingTag, child, closingTag]),
|
|
6588
|
+
[openingTag, indent([hardline, child]), hardline, closingTag],
|
|
6589
|
+
]);
|
|
6544
6590
|
} else if (
|
|
6545
6591
|
child &&
|
|
6546
6592
|
typeof child === 'object' &&
|
package/src/index.test.js
CHANGED
|
@@ -4,6 +4,14 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { languages } from './index.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {typeof prettier & {
|
|
9
|
+
* __debug: {
|
|
10
|
+
* printToDoc: (code: string, options: import('prettier').Options) => Promise<import('prettier').Doc>;
|
|
11
|
+
* };
|
|
12
|
+
* }} PrettierWithDebug
|
|
13
|
+
*/
|
|
14
|
+
|
|
7
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
16
|
const __dirname = dirname(__filename);
|
|
9
17
|
|
|
@@ -62,6 +70,41 @@ describe('prettier-plugin', () => {
|
|
|
62
70
|
});
|
|
63
71
|
};
|
|
64
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} code
|
|
75
|
+
* @param {import('prettier').Options} [options]
|
|
76
|
+
*/
|
|
77
|
+
const printToDoc = async (code, options = {}) => {
|
|
78
|
+
const prettierWithDebug = /** @type {PrettierWithDebug} */ (prettier);
|
|
79
|
+
return await prettierWithDebug.__debug.printToDoc(code, {
|
|
80
|
+
parser: 'ripple',
|
|
81
|
+
plugins: [join(__dirname, 'index.js')],
|
|
82
|
+
...options,
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {unknown} doc
|
|
88
|
+
* @param {(doc: Record<string, unknown>) => boolean} predicate
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
const docContains = (doc, predicate) => {
|
|
92
|
+
if (!doc || typeof doc !== 'object') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(doc)) {
|
|
97
|
+
return doc.some((part) => docContains(part, predicate));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const objectDoc = /** @type {Record<string, unknown>} */ (doc);
|
|
101
|
+
if (predicate(objectDoc)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.values(objectDoc).some((value) => docContains(value, predicate));
|
|
106
|
+
};
|
|
107
|
+
|
|
65
108
|
/**
|
|
66
109
|
* @param {string} code
|
|
67
110
|
* @param {Partial<import('prettier').CursorOptions>} options
|
|
@@ -1055,6 +1098,23 @@ export component Test({ a, b }: Props) {}`;
|
|
|
1055
1098
|
expect(result).toBeWithNewline(expected);
|
|
1056
1099
|
});
|
|
1057
1100
|
|
|
1101
|
+
it('should not force attribute-less elements to break with singleAttributePerLine', async () => {
|
|
1102
|
+
const input = `component One() {
|
|
1103
|
+
<div>"Hello"</div>
|
|
1104
|
+
}`;
|
|
1105
|
+
|
|
1106
|
+
const expected = `component One() {
|
|
1107
|
+
<div>"Hello"</div>
|
|
1108
|
+
}`;
|
|
1109
|
+
|
|
1110
|
+
const result = await format(input, {
|
|
1111
|
+
singleQuote: true,
|
|
1112
|
+
printWidth: 100,
|
|
1113
|
+
singleAttributePerLine: true,
|
|
1114
|
+
});
|
|
1115
|
+
expect(result).toBeWithNewline(expected);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1058
1118
|
it('should respect singleAttributePerLine set to false setting', async () => {
|
|
1059
1119
|
const input = `component One() {
|
|
1060
1120
|
<button
|
|
@@ -3239,6 +3299,114 @@ const items = [] as unknown[];`;
|
|
|
3239
3299
|
expect(result).toBeWithNewline(expected);
|
|
3240
3300
|
});
|
|
3241
3301
|
|
|
3302
|
+
it('should preserve literal newlines in direct double-quoted text children', async () => {
|
|
3303
|
+
const input = `component App() {
|
|
3304
|
+
<pre>"first
|
|
3305
|
+
second"</pre>
|
|
3306
|
+
}`;
|
|
3307
|
+
|
|
3308
|
+
const expected = `component App() {
|
|
3309
|
+
<pre>"first
|
|
3310
|
+
second"</pre>
|
|
3311
|
+
}`;
|
|
3312
|
+
|
|
3313
|
+
const result = await format(input);
|
|
3314
|
+
expect(result).toBeWithNewline(expected);
|
|
3315
|
+
});
|
|
3316
|
+
|
|
3317
|
+
it('should wrap direct double-quoted text children idempotently', async () => {
|
|
3318
|
+
const input = `component App() {
|
|
3319
|
+
<p class="lede">
|
|
3320
|
+
"Set up TSRX with React, Preact, Solid, Vue, or Ripple and then wire in the editor tooling that makes "
|
|
3321
|
+
<code class="inline-code">".tsrx"</code>
|
|
3322
|
+
" files feel native in the rest of your repo."
|
|
3323
|
+
</p>
|
|
3324
|
+
}`;
|
|
3325
|
+
|
|
3326
|
+
const expected = `component App() {
|
|
3327
|
+
<p class="lede">
|
|
3328
|
+
"Set up TSRX with React, Preact, Solid, Vue, or Ripple and then wire in the editor tooling that
|
|
3329
|
+
makes "
|
|
3330
|
+
<code class="inline-code">".tsrx"</code>
|
|
3331
|
+
" files feel native in the rest of your repo."
|
|
3332
|
+
</p>
|
|
3333
|
+
}`;
|
|
3334
|
+
|
|
3335
|
+
const result = await format(input, { printWidth: 100 });
|
|
3336
|
+
const secondResult = await format(result, { printWidth: 100 });
|
|
3337
|
+
expect(result).toBeWithNewline(expected);
|
|
3338
|
+
expect(secondResult).toBeWithNewline(expected);
|
|
3339
|
+
});
|
|
3340
|
+
|
|
3341
|
+
it('should break long direct text children after inline attributes', async () => {
|
|
3342
|
+
const input = `component App() {
|
|
3343
|
+
<span
|
|
3344
|
+
class={styles.notificationMessage}
|
|
3345
|
+
>"The report is ready. Review the summary before sharing it with the team."</span>
|
|
3346
|
+
}`;
|
|
3347
|
+
|
|
3348
|
+
const expected = `component App() {
|
|
3349
|
+
<span class={styles.notificationMessage}>
|
|
3350
|
+
"The report is ready. Review the summary before sharing it with the team."
|
|
3351
|
+
</span>
|
|
3352
|
+
}`;
|
|
3353
|
+
|
|
3354
|
+
const result = await format(input, { printWidth: 80 });
|
|
3355
|
+
expect(result).toBeWithNewline(expected);
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
it('should wrap long direct text children when elements break', async () => {
|
|
3359
|
+
const input = `component App() {
|
|
3360
|
+
<span class={styles.notificationMessage}>"The report is ready. Review the summary before sharing it with the team."</span>
|
|
3361
|
+
}`;
|
|
3362
|
+
|
|
3363
|
+
const expectedPrintWidth70 = `component App() {
|
|
3364
|
+
<span class={styles.notificationMessage}>
|
|
3365
|
+
"The report is ready. Review the summary before sharing it with
|
|
3366
|
+
the team."
|
|
3367
|
+
</span>
|
|
3368
|
+
}`;
|
|
3369
|
+
const expectedPrintWidth40 = `component App() {
|
|
3370
|
+
<span
|
|
3371
|
+
class={styles.notificationMessage}
|
|
3372
|
+
>
|
|
3373
|
+
"The report is ready. Review the
|
|
3374
|
+
summary before sharing it with the
|
|
3375
|
+
team."
|
|
3376
|
+
</span>
|
|
3377
|
+
}`;
|
|
3378
|
+
|
|
3379
|
+
const resultPrintWidth70 = await format(input, { printWidth: 70 });
|
|
3380
|
+
expect(resultPrintWidth70).toBeWithNewline(expectedPrintWidth70);
|
|
3381
|
+
|
|
3382
|
+
const resultPrintWidth40 = await format(input, { printWidth: 40 });
|
|
3383
|
+
expect(resultPrintWidth40).toBeWithNewline(expectedPrintWidth40);
|
|
3384
|
+
});
|
|
3385
|
+
|
|
3386
|
+
it('should keep direct text children on the text-specific conditional layout path', async () => {
|
|
3387
|
+
const input = `component App() {
|
|
3388
|
+
<div>"The report is ready. Review the summary before sharing it with the team."</div>
|
|
3389
|
+
}`;
|
|
3390
|
+
|
|
3391
|
+
const doc = await printToDoc(input, { printWidth: 70 });
|
|
3392
|
+
const hasConditionalTextGroup = docContains(doc, (part) => {
|
|
3393
|
+
return (
|
|
3394
|
+
part.type === 'group' &&
|
|
3395
|
+
Array.isArray(part.expandedStates) &&
|
|
3396
|
+
docContains(part.contents, (childPart) => {
|
|
3397
|
+
return (
|
|
3398
|
+
childPart.type === 'fill' &&
|
|
3399
|
+
Array.isArray(childPart.parts) &&
|
|
3400
|
+
childPart.parts.some((part) => Array.isArray(part) && part.includes('"The')) &&
|
|
3401
|
+
childPart.parts.some((part) => Array.isArray(part) && part.includes('team."'))
|
|
3402
|
+
);
|
|
3403
|
+
})
|
|
3404
|
+
);
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
expect(hasConditionalTextGroup).toBe(true);
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3242
3410
|
it('should not insert a new line between js and jsx if not provided', async () => {
|
|
3243
3411
|
const expected = `export component App() {
|
|
3244
3412
|
let text = 'something';
|
|
@@ -3474,6 +3642,20 @@ const items = [] as unknown[];`;
|
|
|
3474
3642
|
expect(result).toBeWithNewline(expected);
|
|
3475
3643
|
});
|
|
3476
3644
|
|
|
3645
|
+
it('should break long nested TypeScript conditional type aliases', async () => {
|
|
3646
|
+
const input = `type PageModelValue<Value> = Value extends ReadonlySignal<unknown> ? Value : Value extends (...args: any[]) => any ? Value : Value extends object ? { [Key in keyof Value]: PageModelValue<Value[Key]> } : never;`;
|
|
3647
|
+
const expected = `type PageModelValue<Value> =
|
|
3648
|
+
Value extends ReadonlySignal<unknown>
|
|
3649
|
+
? Value
|
|
3650
|
+
: Value extends (...args: any[]) => any
|
|
3651
|
+
? Value
|
|
3652
|
+
: Value extends object
|
|
3653
|
+
? { [Key in keyof Value]: PageModelValue<Value[Key]> }
|
|
3654
|
+
: never;`;
|
|
3655
|
+
const result = await format(input);
|
|
3656
|
+
expect(result).toBeWithNewline(expected);
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3477
3659
|
it('should format TypeScript mapped types (TSMappedType)', async () => {
|
|
3478
3660
|
const input = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] }`;
|
|
3479
3661
|
const expected = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] };`;
|