@tsrx/prettier-plugin 0.3.63 → 0.3.65

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.63",
3
+ "version": "0.3.65",
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.12"
30
+ "@tsrx/core": "0.1.13"
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
 
@@ -1506,6 +1506,15 @@ function printRippleNode(node, path, options, print, args) {
1506
1506
  break;
1507
1507
  }
1508
1508
 
1509
+ case 'TSSatisfiesExpression': {
1510
+ nodeContent = [
1511
+ path.call(print, 'expression'),
1512
+ ' satisfies ',
1513
+ path.call(print, 'typeAnnotation'),
1514
+ ];
1515
+ break;
1516
+ }
1517
+
1509
1518
  case 'TSNonNullExpression': {
1510
1519
  nodeContent = [path.call(print, 'expression'), '!'];
1511
1520
  break;
@@ -1923,8 +1932,23 @@ function printRippleNode(node, path, options, print, args) {
1923
1932
  case 'LogicalExpression': {
1924
1933
  const logicalParent = path.getParentNode();
1925
1934
  let logicalResult;
1926
- // Don't add indent if we're in a conditional test context
1927
- if (args?.isConditionalTest) {
1935
+ const rightIsNullLiteral = node.right.type === 'Literal' && node.right.value === null;
1936
+ const shouldKeepNullishFallbackInline =
1937
+ node.operator === '??' &&
1938
+ rightIsNullLiteral &&
1939
+ (node.left.type === 'CallExpression' ||
1940
+ node.left.type === 'ChainExpression' ||
1941
+ node.left.type === 'NewExpression');
1942
+ if (shouldKeepNullishFallbackInline) {
1943
+ logicalResult = group([
1944
+ path.call(print, 'left'),
1945
+ ' ',
1946
+ node.operator,
1947
+ ' ',
1948
+ path.call(print, 'right'),
1949
+ ]);
1950
+ } else if (args?.isConditionalTest) {
1951
+ // Don't add indent if we're in a conditional test context
1928
1952
  logicalResult = group([
1929
1953
  path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
1930
1954
  ' ',
@@ -2814,36 +2838,40 @@ function printArrowFunction(node, path, options, print, args) {
2814
2838
  parts.push(': ', path.call(print, 'returnType'));
2815
2839
  }
2816
2840
 
2817
- parts.push(' => ');
2818
-
2819
2841
  // For block statements, print the body directly to get proper formatting
2820
2842
  if (node.body.type === 'BlockStatement') {
2843
+ parts.push(' => ');
2821
2844
  parts.push(path.call(print, 'body'));
2822
2845
  } else {
2823
2846
  // For expression bodies, check if we need to wrap in parens
2824
2847
  // Wrap ObjectExpression, AssignmentExpression, and SequenceExpression in parens
2825
2848
  // to avoid ambiguity with block statements or to clarify intent
2826
2849
  const bodyDoc = path.call(print, 'body');
2850
+ const groupId = Symbol('arrow');
2851
+ const shouldBreakBody = shouldBreakArrowExpressionBody(node.body, options, args);
2852
+ /** @type {Doc | Doc[]} */
2853
+ let bodyContent;
2827
2854
  if (
2828
2855
  node.body.type === 'ObjectExpression' ||
2829
2856
  node.body.type === 'AssignmentExpression' ||
2830
- node.body.type === 'SequenceExpression' ||
2831
- (args?.isInAttribute && isTemplateExpression(node.body))
2857
+ node.body.type === 'SequenceExpression'
2832
2858
  ) {
2833
- parts.push('(');
2834
- if (isTemplateExpression(node.body)) {
2835
- parts.push(indent([hardline, bodyDoc]));
2836
- parts.push(hardline);
2837
- } else {
2838
- parts.push(bodyDoc);
2839
- }
2840
- parts.push(')');
2859
+ bodyContent = ['(', bodyDoc, ')'];
2841
2860
  } else {
2842
- parts.push(bodyDoc);
2861
+ bodyContent = bodyDoc;
2862
+ }
2863
+ if (shouldBreakBody) {
2864
+ parts.push(' =>', indent([hardline, bodyContent]));
2865
+ } else {
2866
+ parts.push(
2867
+ ' =>',
2868
+ group(indent(line), { id: groupId }),
2869
+ indentIfBreak(bodyContent, { groupId }),
2870
+ );
2843
2871
  }
2844
2872
  }
2845
2873
 
2846
- return parts;
2874
+ return group(parts);
2847
2875
  }
2848
2876
 
2849
2877
  /**
@@ -2861,6 +2889,15 @@ function isTemplateExpression(node) {
2861
2889
  );
2862
2890
  }
2863
2891
 
2892
+ /**
2893
+ * Check whether a braced attribute expression should close on its own line.
2894
+ * @param {AST.Node} node - The expression inside the attribute braces
2895
+ * @returns {boolean}
2896
+ */
2897
+ function shouldBreakAttributeExpressionClosingBrace(node) {
2898
+ return node.type === 'ArrowFunctionExpression' && node.body && isTemplateExpression(node.body);
2899
+ }
2900
+
2864
2901
  /**
2865
2902
  * Print an export default declaration
2866
2903
  * @param {AST.ExportDefaultDeclaration} node - The export default node
@@ -3053,6 +3090,37 @@ function shouldHugArrowFunctions(args) {
3053
3090
  return firstBlockIndex === 0;
3054
3091
  }
3055
3092
 
3093
+ /**
3094
+ * Check whether a node's original source span exceeds the configured print width.
3095
+ * @param {AST.NodeWithLocation} node - The node to check
3096
+ * @param {RippleFormatOptions} options - Prettier options
3097
+ * @returns {boolean}
3098
+ */
3099
+ function sourceSpanExceedsPrintWidth(node, options) {
3100
+ const printWidth = options.printWidth ?? 80;
3101
+ if (!options.originalText || node.start === undefined || node.end === undefined) {
3102
+ return false;
3103
+ }
3104
+ return options.originalText.slice(node.start, node.end).length > printWidth;
3105
+ }
3106
+
3107
+ /**
3108
+ * Check if an arrow expression body should break immediately after `=>`.
3109
+ * @param {AST.Expression} node - The arrow body expression
3110
+ * @param {RippleFormatOptions} options - Prettier options
3111
+ * @param {PrintArgs} [args] - Additional context arguments
3112
+ * @returns {boolean}
3113
+ */
3114
+ function shouldBreakArrowExpressionBody(node, options, args) {
3115
+ if (args?.isInAttribute && isTemplateExpression(node)) {
3116
+ return true;
3117
+ }
3118
+ return (
3119
+ (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
3120
+ sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
3121
+ );
3122
+ }
3123
+
3056
3124
  /**
3057
3125
  * Print call expression arguments
3058
3126
  * @param {AstPath<AST.CallExpression>} path - The call path
@@ -5727,7 +5795,7 @@ function printTsx(node, path, options, print) {
5727
5795
  return [tagName, closingTagName];
5728
5796
  }
5729
5797
 
5730
- if (printedChildren.length > 1) {
5798
+ if (!is_fragment || printedChildren.length > 1) {
5731
5799
  return group([
5732
5800
  tagName,
5733
5801
  indent([hardline, join(hardline, printedChildren)]),
@@ -6141,11 +6209,15 @@ function printJSXAttribute(attr, path, options, print) {
6141
6209
  }
6142
6210
 
6143
6211
  if (attr.value.type === 'JSXExpressionContainer') {
6212
+ const expression = attr.value.expression;
6144
6213
  const exprDoc = path.call(
6145
6214
  (valuePath) => print(valuePath, { isInAttribute: true }),
6146
6215
  'value',
6147
6216
  'expression',
6148
6217
  );
6218
+ if (shouldBreakAttributeExpressionClosingBrace(expression)) {
6219
+ return [name, '={', exprDoc, hardline, '}'];
6220
+ }
6149
6221
  return [name, '={', exprDoc, '}'];
6150
6222
  }
6151
6223
 
@@ -6723,6 +6795,9 @@ function printAttribute(node, path, options, print) {
6723
6795
  parts.push('={');
6724
6796
  // Pass inline context for attribute values (keep objects compact)
6725
6797
  parts.push(path.call((attrPath) => print(attrPath, { isInAttribute: true }), 'value'));
6798
+ if (shouldBreakAttributeExpressionClosingBrace(node.value)) {
6799
+ parts.push(hardline);
6800
+ }
6726
6801
  parts.push('}');
6727
6802
  }
6728
6803
  }
package/src/index.test.js CHANGED
@@ -200,6 +200,58 @@ describe('prettier-plugin', () => {
200
200
  expect(result).toBeWithNewline(expected);
201
201
  });
202
202
 
203
+ it('should format explicit tsx arrow returns like tsrx blocks', async () => {
204
+ const input = `component Test(props) {
205
+ const func = (item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>;
206
+
207
+ <List
208
+ items={props.items}
209
+ renderItem={(item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>}
210
+ />
211
+ }`;
212
+ const expected = `component Test(props) {
213
+ const func = (item) => <tsx>
214
+ <ItemView item={item} onSelect={props.onSelect} />
215
+ </tsx>;
216
+
217
+ <List
218
+ items={props.items}
219
+ renderItem={(item) =>
220
+ <tsx>
221
+ <ItemView item={item} onSelect={props.onSelect} />
222
+ </tsx>
223
+ }
224
+ />
225
+ }`;
226
+ const result = await format(input);
227
+ expect(result).toBeWithNewline(expected);
228
+ });
229
+
230
+ it('should format template arrow returns in tsx attributes like ripple attributes', async () => {
231
+ const input = `component Test(props) {
232
+ const view = <tsx>
233
+ <List
234
+ items={props.items}
235
+ renderItem={(item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>}
236
+ />
237
+ </tsx>;
238
+ }`;
239
+ const expected = `component Test(props) {
240
+ const view = <tsx>
241
+ <List
242
+ items={props.items}
243
+ renderItem={(item) =>
244
+ <tsx>
245
+ <ItemView item={item} onSelect={props.onSelect} />
246
+ </tsx>
247
+ }
248
+ />
249
+ </tsx>;
250
+ }`;
251
+ const result = await format(input);
252
+ expect(result).toBeWithNewline(expected);
253
+ });
254
+
203
255
  it('should preserve comments before expressions after nested tsx and tsrx blocks', async () => {
204
256
  const input = `component App() {
205
257
  const content = <tsx>
@@ -255,7 +307,7 @@ describe('prettier-plugin', () => {
255
307
  }`,
256
308
  expected: `component Test() {
257
309
  <A
258
- fallback={(error) => (
310
+ fallback={(error) =>
259
311
  <>
260
312
  <B
261
313
  id="xyz"
@@ -265,7 +317,7 @@ describe('prettier-plugin', () => {
265
317
  otherProp={2}
266
318
  />
267
319
  </>
268
- )}
320
+ }
269
321
  />
270
322
  }`,
271
323
  },
@@ -279,7 +331,7 @@ describe('prettier-plugin', () => {
279
331
  }`,
280
332
  expected: `component Test() {
281
333
  <A
282
- fallback={(error) => (
334
+ fallback={(error) =>
283
335
  <tsx>
284
336
  <B
285
337
  id="xyz"
@@ -289,7 +341,7 @@ describe('prettier-plugin', () => {
289
341
  otherProp={2}
290
342
  />
291
343
  </tsx>
292
- )}
344
+ }
293
345
  />
294
346
  }`,
295
347
  },
@@ -303,7 +355,7 @@ describe('prettier-plugin', () => {
303
355
  }`,
304
356
  expected: `component Test() {
305
357
  <A
306
- fallback={(error) => (
358
+ fallback={(error) =>
307
359
  <tsrx>
308
360
  <B
309
361
  id="xyz"
@@ -313,7 +365,7 @@ describe('prettier-plugin', () => {
313
365
  otherProp={2}
314
366
  />
315
367
  </tsrx>
316
- )}
368
+ }
317
369
  />
318
370
  }`,
319
371
  },
@@ -327,7 +379,7 @@ describe('prettier-plugin', () => {
327
379
  }`,
328
380
  expected: `component Test() {
329
381
  <A
330
- fallback={(error) => (
382
+ fallback={(error) =>
331
383
  <tsx:react>
332
384
  <B
333
385
  id="xyz"
@@ -337,7 +389,7 @@ describe('prettier-plugin', () => {
337
389
  otherProp={2}
338
390
  />
339
391
  </tsx:react>
340
- )}
392
+ }
341
393
  />
342
394
  }`,
343
395
  },
@@ -1015,7 +1067,9 @@ import { Something, type Props, track } from 'ripple';`;
1015
1067
  const foo = <tsx><Bar {...props} /></tsx>;`;
1016
1068
 
1017
1069
  const expected = `const props = {};
1018
- const foo = <tsx><Bar {...props} /></tsx>;`;
1070
+ const foo = <tsx>
1071
+ <Bar {...props} />
1072
+ </tsx>;`;
1019
1073
 
1020
1074
  const result = await format(input, { singleQuote: true });
1021
1075
  expect(result).toBeWithNewline(expected);
@@ -1380,6 +1434,31 @@ import { effect, track } from 'ripple';`;
1380
1434
  expect(result).toBeWithNewline(expected);
1381
1435
  });
1382
1436
 
1437
+ it('should break arrow before a long generic optional call with nullish fallback', async () => {
1438
+ const input = `const test = () => menuRef.current?.querySelector<HTMLElement>(
1439
+ "[role=\\"menuitem\\"]:not([aria-disabled=\\"true\\"])",
1440
+ ) ??
1441
+ null`;
1442
+ const expected = `const test = () =>
1443
+ menuRef.current?.querySelector<HTMLElement>(
1444
+ '[role="menuitem"]:not([aria-disabled="true"])',
1445
+ ) ?? null;`;
1446
+ const result = await format(input, { singleQuote: true });
1447
+ expect(result).toBeWithNewline(expected);
1448
+ });
1449
+
1450
+ it('keeps nullish fallback inline in a conditional test', async () => {
1451
+ const input = `const test = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])') ?? null ? a : b;`;
1452
+ const expected = `const test =
1453
+ menuRef.current?.querySelector<HTMLElement>(
1454
+ '[role="menuitem"]:not([aria-disabled="true"])',
1455
+ ) ?? null
1456
+ ? a
1457
+ : b;`;
1458
+ const result = await format(input, { singleQuote: true });
1459
+ expect(result).toBeWithNewline(expected);
1460
+ });
1461
+
1383
1462
  it('does not add spaces around inlined array elements in destructured arguments', async () => {
1384
1463
  const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
1385
1464
  }
@@ -2254,7 +2333,9 @@ files = [...(files ?? []), ...dt.files];`;
2254
2333
 
2255
2334
  const expected = `class Foo {
2256
2335
  bar() {
2257
- return <tsx>{'Hello'}</tsx>;
2336
+ return <tsx>
2337
+ {'Hello'}
2338
+ </tsx>;
2258
2339
  }
2259
2340
  }`;
2260
2341
 
@@ -2613,6 +2694,35 @@ component Child({ something }) {
2613
2694
  expect(result).toBeWithNewline(expected);
2614
2695
  });
2615
2696
 
2697
+ it('prints satisfies expressions in switch default cases', async () => {
2698
+ const input = `export component Test(props: { status: "ok" | "error" }) {
2699
+ switch (props.status) {
2700
+ case "ok":
2701
+ <div>"ok"</div>
2702
+ return
2703
+ case "error":
2704
+ <div>"error"</div>
2705
+ return
2706
+ default:
2707
+ props.status satisfies never
2708
+ }
2709
+ }`;
2710
+ const expected = `export component Test(props: { status: "ok" | "error" }) {
2711
+ switch (props.status) {
2712
+ case "ok":
2713
+ <div>"ok"</div>
2714
+ return;
2715
+ case "error":
2716
+ <div>"error"</div>
2717
+ return;
2718
+ default:
2719
+ props.status satisfies never;
2720
+ }
2721
+ }`;
2722
+ const result = await format(input);
2723
+ expect(result).toBeWithNewline(expected);
2724
+ });
2725
+
2616
2726
  it('prints function with a rest parameter correctly', async () => {
2617
2727
  const expected = `function TestRest(...args: string[]) {
2618
2728
  console.log(args);