@tsrx/prettier-plugin 0.3.62 → 0.3.64
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 +130 -12
- package/src/index.test.js +78 -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.64",
|
|
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.12"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src/"
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @typedef {((path: AstPath) => Doc) & ((path: AstPath, args: PrintArgs) => Doc)} PrintFn
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/** @typedef {Partial<Pick<ParserOptions, 'singleQuote' | 'jsxSingleQuote' | 'semi' | 'trailingComma' | 'useTabs' | 'tabWidth' | 'singleAttributePerLine' | 'bracketSameLine' | 'bracketSpacing' | 'arrowParens' | 'originalText'>> & { locStart: (node: AST.NodeWithLocation) => number, locEnd: (node: AST.NodeWithLocation) => number }} RippleFormatOptions */
|
|
17
|
+
/** @typedef {Partial<Pick<ParserOptions, 'singleQuote' | 'jsxSingleQuote' | 'semi' | 'trailingComma' | 'useTabs' | 'tabWidth' | 'singleAttributePerLine' | 'bracketSameLine' | 'bracketSpacing' | 'arrowParens' | 'originalText' | 'printWidth'>> & { locStart: (node: AST.NodeWithLocation) => number, locEnd: (node: AST.NodeWithLocation) => number }} RippleFormatOptions */
|
|
18
18
|
|
|
19
19
|
/** @typedef {{ isInAttribute?: boolean, isInArray?: boolean, allowInlineObject?: boolean, isConditionalTest?: boolean, isNestedConditional?: boolean, suppressLeadingComments?: boolean, suppressExpressionLeadingComments?: boolean, isInlineContext?: boolean, isStatement?: boolean, isLogicalAndOr?: boolean, allowShorthandProperty?: boolean, isFirstChild?: boolean, skipComponentLabel?: boolean, noBreakInside?: boolean, expandLastArg?: boolean }} PrintArgs */
|
|
20
20
|
|
|
@@ -360,6 +360,49 @@ function binaryExpressionNeedsParens(node, parent) {
|
|
|
360
360
|
return false;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Check if a parenthesized AssignmentExpression needs its parentheses preserved.
|
|
365
|
+
* @param {AST.AssignmentExpression} node - The expression node
|
|
366
|
+
* @param {AST.Node | null} parent - The parent node
|
|
367
|
+
* @returns {boolean} - True if parentheses are needed
|
|
368
|
+
*/
|
|
369
|
+
function assignmentExpressionNeedsParens(node, parent) {
|
|
370
|
+
if (!node.metadata?.parenthesized || !parent) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (parent.type === 'BinaryExpression' || parent.type === 'LogicalExpression') {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (parent.type === 'ConditionalExpression') {
|
|
379
|
+
return parent.test === node;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (parent.type === 'AwaitExpression' || parent.type === 'YieldExpression') {
|
|
383
|
+
return parent.argument === node;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
|
|
387
|
+
return parent.callee === node;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (parent.type === 'TaggedTemplateExpression') {
|
|
391
|
+
return parent.tag === node;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (
|
|
395
|
+
parent.type === 'TSAsExpression' ||
|
|
396
|
+
parent.type === 'TSSatisfiesExpression' ||
|
|
397
|
+
parent.type === 'TSNonNullExpression' ||
|
|
398
|
+
parent.type === 'TSInstantiationExpression'
|
|
399
|
+
) {
|
|
400
|
+
return parent.expression === node;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
363
406
|
/**
|
|
364
407
|
* Create a function that skips specified characters in text
|
|
365
408
|
* @param {string | RegExp} characters - Characters to skip
|
|
@@ -718,11 +761,13 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
718
761
|
const isInlineContext = args && args.isInlineContext;
|
|
719
762
|
const suppressLeadingComments = args && args.suppressLeadingComments;
|
|
720
763
|
const suppressExpressionLeadingComments = args && args.suppressExpressionLeadingComments;
|
|
764
|
+
const parentNode = /** @type {AST.Node | null} */ (path.getParentNode());
|
|
721
765
|
|
|
722
766
|
// For TSRXExpression, Text, and Html nodes, don't add leading comments here - they should be handled
|
|
723
|
-
// as separate children within
|
|
767
|
+
// as separate children within elements, not as part of the expression.
|
|
724
768
|
const shouldSkipLeadingComments =
|
|
725
|
-
|
|
769
|
+
parentNode?.type === 'Element' &&
|
|
770
|
+
(node.type === 'TSRXExpression' || node.type === 'Text' || node.type === 'Html');
|
|
726
771
|
|
|
727
772
|
// Handle leading comments
|
|
728
773
|
if (node.leadingComments && !shouldSkipLeadingComments && !suppressLeadingComments) {
|
|
@@ -1335,6 +1380,10 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1335
1380
|
group(indent(line), { id: groupId }),
|
|
1336
1381
|
indentIfBreak(rightSide, { groupId }),
|
|
1337
1382
|
]);
|
|
1383
|
+
const parent = path.getParentNode();
|
|
1384
|
+
if (assignmentExpressionNeedsParens(node, parent)) {
|
|
1385
|
+
nodeContent = ['(', nodeContent, ')'];
|
|
1386
|
+
}
|
|
1338
1387
|
break;
|
|
1339
1388
|
}
|
|
1340
1389
|
|
|
@@ -1874,6 +1923,13 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1874
1923
|
case 'LogicalExpression': {
|
|
1875
1924
|
const logicalParent = path.getParentNode();
|
|
1876
1925
|
let logicalResult;
|
|
1926
|
+
const rightIsNullLiteral = node.right.type === 'Literal' && node.right.value === null;
|
|
1927
|
+
const shouldKeepNullishFallbackInline =
|
|
1928
|
+
node.operator === '??' &&
|
|
1929
|
+
rightIsNullLiteral &&
|
|
1930
|
+
(node.left.type === 'CallExpression' ||
|
|
1931
|
+
node.left.type === 'ChainExpression' ||
|
|
1932
|
+
node.left.type === 'NewExpression');
|
|
1877
1933
|
// Don't add indent if we're in a conditional test context
|
|
1878
1934
|
if (args?.isConditionalTest) {
|
|
1879
1935
|
logicalResult = group([
|
|
@@ -1882,6 +1938,14 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1882
1938
|
node.operator,
|
|
1883
1939
|
[line, path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right')],
|
|
1884
1940
|
]);
|
|
1941
|
+
} else if (shouldKeepNullishFallbackInline) {
|
|
1942
|
+
logicalResult = group([
|
|
1943
|
+
path.call(print, 'left'),
|
|
1944
|
+
' ',
|
|
1945
|
+
node.operator,
|
|
1946
|
+
' ',
|
|
1947
|
+
path.call(print, 'right'),
|
|
1948
|
+
]);
|
|
1885
1949
|
} else {
|
|
1886
1950
|
logicalResult = group([
|
|
1887
1951
|
path.call(print, 'left'),
|
|
@@ -2765,36 +2829,45 @@ function printArrowFunction(node, path, options, print, args) {
|
|
|
2765
2829
|
parts.push(': ', path.call(print, 'returnType'));
|
|
2766
2830
|
}
|
|
2767
2831
|
|
|
2768
|
-
parts.push(' => ');
|
|
2769
|
-
|
|
2770
2832
|
// For block statements, print the body directly to get proper formatting
|
|
2771
2833
|
if (node.body.type === 'BlockStatement') {
|
|
2834
|
+
parts.push(' => ');
|
|
2772
2835
|
parts.push(path.call(print, 'body'));
|
|
2773
2836
|
} else {
|
|
2774
2837
|
// For expression bodies, check if we need to wrap in parens
|
|
2775
2838
|
// Wrap ObjectExpression, AssignmentExpression, and SequenceExpression in parens
|
|
2776
2839
|
// to avoid ambiguity with block statements or to clarify intent
|
|
2777
2840
|
const bodyDoc = path.call(print, 'body');
|
|
2841
|
+
const groupId = Symbol('arrow');
|
|
2842
|
+
const shouldBreakBody = shouldBreakArrowExpressionBody(node.body, options);
|
|
2843
|
+
/** @type {Doc | Doc[]} */
|
|
2844
|
+
let bodyContent;
|
|
2778
2845
|
if (
|
|
2779
2846
|
node.body.type === 'ObjectExpression' ||
|
|
2780
2847
|
node.body.type === 'AssignmentExpression' ||
|
|
2781
2848
|
node.body.type === 'SequenceExpression' ||
|
|
2782
2849
|
(args?.isInAttribute && isTemplateExpression(node.body))
|
|
2783
2850
|
) {
|
|
2784
|
-
parts.push('(');
|
|
2785
2851
|
if (isTemplateExpression(node.body)) {
|
|
2786
|
-
|
|
2787
|
-
parts.push(hardline);
|
|
2852
|
+
bodyContent = ['(', indent([hardline, bodyDoc]), hardline, ')'];
|
|
2788
2853
|
} else {
|
|
2789
|
-
|
|
2854
|
+
bodyContent = ['(', bodyDoc, ')'];
|
|
2790
2855
|
}
|
|
2791
|
-
parts.push(')');
|
|
2792
2856
|
} else {
|
|
2793
|
-
|
|
2857
|
+
bodyContent = bodyDoc;
|
|
2858
|
+
}
|
|
2859
|
+
if (shouldBreakBody) {
|
|
2860
|
+
parts.push(' =>', indent([hardline, bodyContent]));
|
|
2861
|
+
} else {
|
|
2862
|
+
parts.push(
|
|
2863
|
+
' =>',
|
|
2864
|
+
group(indent(line), { id: groupId }),
|
|
2865
|
+
indentIfBreak(bodyContent, { groupId }),
|
|
2866
|
+
);
|
|
2794
2867
|
}
|
|
2795
2868
|
}
|
|
2796
2869
|
|
|
2797
|
-
return parts;
|
|
2870
|
+
return group(parts);
|
|
2798
2871
|
}
|
|
2799
2872
|
|
|
2800
2873
|
/**
|
|
@@ -3004,6 +3077,33 @@ function shouldHugArrowFunctions(args) {
|
|
|
3004
3077
|
return firstBlockIndex === 0;
|
|
3005
3078
|
}
|
|
3006
3079
|
|
|
3080
|
+
/**
|
|
3081
|
+
* Check whether a node's original source span exceeds the configured print width.
|
|
3082
|
+
* @param {AST.NodeWithLocation} node - The node to check
|
|
3083
|
+
* @param {RippleFormatOptions} options - Prettier options
|
|
3084
|
+
* @returns {boolean}
|
|
3085
|
+
*/
|
|
3086
|
+
function sourceSpanExceedsPrintWidth(node, options) {
|
|
3087
|
+
const printWidth = options.printWidth ?? 80;
|
|
3088
|
+
if (!options.originalText || node.start === undefined || node.end === undefined) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
return options.originalText.slice(node.start, node.end).length > printWidth;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/**
|
|
3095
|
+
* Check if an arrow expression body should break immediately after `=>`.
|
|
3096
|
+
* @param {AST.Expression} node - The arrow body expression
|
|
3097
|
+
* @param {RippleFormatOptions} options - Prettier options
|
|
3098
|
+
* @returns {boolean}
|
|
3099
|
+
*/
|
|
3100
|
+
function shouldBreakArrowExpressionBody(node, options) {
|
|
3101
|
+
return (
|
|
3102
|
+
(node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
|
|
3103
|
+
sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3007
3107
|
/**
|
|
3008
3108
|
* Print call expression arguments
|
|
3009
3109
|
* @param {AstPath<AST.CallExpression>} path - The call path
|
|
@@ -5930,6 +6030,24 @@ function printJSXElement(node, path, options, print) {
|
|
|
5930
6030
|
// Accumulate text content, preserving spaces between words
|
|
5931
6031
|
const trimmed = child.value.trim();
|
|
5932
6032
|
if (trimmed) {
|
|
6033
|
+
const nextChild = node.children[i + 1];
|
|
6034
|
+
const afterNextChild = node.children[i + 2];
|
|
6035
|
+
const nextText = afterNextChild?.type === 'JSXText' ? afterNextChild.value.trim() : '';
|
|
6036
|
+
if (
|
|
6037
|
+
tagName === 'tsrx' &&
|
|
6038
|
+
trimmed.endsWith('=') &&
|
|
6039
|
+
nextChild?.type === 'JSXElement' &&
|
|
6040
|
+
nextText === ';'
|
|
6041
|
+
) {
|
|
6042
|
+
if (currentText) {
|
|
6043
|
+
childrenDocs.push(currentText);
|
|
6044
|
+
currentText = '';
|
|
6045
|
+
}
|
|
6046
|
+
childrenDocs.push([trimmed, ' ', path.call(print, 'children', i + 1), ';']);
|
|
6047
|
+
i += 2;
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
|
|
5933
6051
|
if (currentText) {
|
|
5934
6052
|
currentText += ' ' + trimmed;
|
|
5935
6053
|
} else {
|
package/src/index.test.js
CHANGED
|
@@ -200,6 +200,49 @@ describe('prettier-plugin', () => {
|
|
|
200
200
|
expect(result).toBeWithNewline(expected);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it('should preserve comments before expressions after nested tsx and tsrx blocks', async () => {
|
|
204
|
+
const input = `component App() {
|
|
205
|
+
const content = <tsx>
|
|
206
|
+
{<tsrx>
|
|
207
|
+
const nested =
|
|
208
|
+
<tsx>
|
|
209
|
+
<span class="nested-tsx">
|
|
210
|
+
{'inside nested tsx'}
|
|
211
|
+
</span>
|
|
212
|
+
</tsx>
|
|
213
|
+
;
|
|
214
|
+
<div class="native">{nested}</div>
|
|
215
|
+
</tsrx>}
|
|
216
|
+
</tsx>;
|
|
217
|
+
|
|
218
|
+
// const content = <tsrx>
|
|
219
|
+
// <div>{hey()}</div>
|
|
220
|
+
// </tsrx>;
|
|
221
|
+
|
|
222
|
+
{content}
|
|
223
|
+
}`;
|
|
224
|
+
const expected = `component App() {
|
|
225
|
+
const content = <tsx>
|
|
226
|
+
{<tsrx>
|
|
227
|
+
const nested = <tsx>
|
|
228
|
+
<span class="nested-tsx">
|
|
229
|
+
{'inside nested tsx'}
|
|
230
|
+
</span>
|
|
231
|
+
</tsx>;
|
|
232
|
+
<div class="native">{nested}</div>
|
|
233
|
+
</tsrx>}
|
|
234
|
+
</tsx>;
|
|
235
|
+
|
|
236
|
+
// const content = <tsrx>
|
|
237
|
+
// <div>{hey()}</div>
|
|
238
|
+
// </tsrx>;
|
|
239
|
+
|
|
240
|
+
{content}
|
|
241
|
+
}`;
|
|
242
|
+
const result = await format(input, { singleQuote: true });
|
|
243
|
+
expect(result).toBeWithNewline(expected);
|
|
244
|
+
});
|
|
245
|
+
|
|
203
246
|
it('should break nested TSX element attributes inside expression props', async () => {
|
|
204
247
|
const cases = [
|
|
205
248
|
{
|
|
@@ -762,6 +805,28 @@ function test() {
|
|
|
762
805
|
expect(result).toBeWithNewline(expected);
|
|
763
806
|
});
|
|
764
807
|
|
|
808
|
+
it('should preserve required parentheses around assignment expressions', async () => {
|
|
809
|
+
const input = `const openSignal = useRef<Signal<boolean> | null>(null)
|
|
810
|
+
const open = props.open ?? (openSignal.current ??= signal(false))
|
|
811
|
+
const sum = a + (b = c)
|
|
812
|
+
const condition = (a = b) ? c : d
|
|
813
|
+
const called = (factory = getFactory())()
|
|
814
|
+
async function load() {
|
|
815
|
+
await (promise = getPromise())
|
|
816
|
+
}`;
|
|
817
|
+
const expected = `const openSignal = useRef<Signal<boolean> | null>(null);
|
|
818
|
+
const open = props.open ?? (openSignal.current ??= signal(false));
|
|
819
|
+
const sum = a + (b = c);
|
|
820
|
+
const condition = (a = b) ? c : d;
|
|
821
|
+
const called = (factory = getFactory())();
|
|
822
|
+
async function load() {
|
|
823
|
+
await (promise = getPromise());
|
|
824
|
+
}`;
|
|
825
|
+
|
|
826
|
+
const result = await format(input, { singleQuote: true });
|
|
827
|
+
expect(result).toBeWithNewline(expected);
|
|
828
|
+
});
|
|
829
|
+
|
|
765
830
|
it('should not change formatting for function object properties and properties in square brackets', async () => {
|
|
766
831
|
const expected = `export component App() {
|
|
767
832
|
const SYMBOL_PROP = Symbol();
|
|
@@ -1315,6 +1380,19 @@ import { effect, track } from 'ripple';`;
|
|
|
1315
1380
|
expect(result).toBeWithNewline(expected);
|
|
1316
1381
|
});
|
|
1317
1382
|
|
|
1383
|
+
it('should break arrow before a long generic optional call with nullish fallback', async () => {
|
|
1384
|
+
const input = `const test = () => menuRef.current?.querySelector<HTMLElement>(
|
|
1385
|
+
"[role=\\"menuitem\\"]:not([aria-disabled=\\"true\\"])",
|
|
1386
|
+
) ??
|
|
1387
|
+
null`;
|
|
1388
|
+
const expected = `const test = () =>
|
|
1389
|
+
menuRef.current?.querySelector<HTMLElement>(
|
|
1390
|
+
'[role="menuitem"]:not([aria-disabled="true"])',
|
|
1391
|
+
) ?? null;`;
|
|
1392
|
+
const result = await format(input, { singleQuote: true });
|
|
1393
|
+
expect(result).toBeWithNewline(expected);
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1318
1396
|
it('does not add spaces around inlined array elements in destructured arguments', async () => {
|
|
1319
1397
|
const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
|
|
1320
1398
|
}
|