@tsrx/prettier-plugin 0.3.63 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsrx/prettier-plugin",
3
- "version": "0.3.63",
3
+ "version": "0.3.64",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
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
 
@@ -1923,6 +1923,13 @@ function printRippleNode(node, path, options, print, args) {
1923
1923
  case 'LogicalExpression': {
1924
1924
  const logicalParent = path.getParentNode();
1925
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');
1926
1933
  // Don't add indent if we're in a conditional test context
1927
1934
  if (args?.isConditionalTest) {
1928
1935
  logicalResult = group([
@@ -1931,6 +1938,14 @@ function printRippleNode(node, path, options, print, args) {
1931
1938
  node.operator,
1932
1939
  [line, path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right')],
1933
1940
  ]);
1941
+ } else if (shouldKeepNullishFallbackInline) {
1942
+ logicalResult = group([
1943
+ path.call(print, 'left'),
1944
+ ' ',
1945
+ node.operator,
1946
+ ' ',
1947
+ path.call(print, 'right'),
1948
+ ]);
1934
1949
  } else {
1935
1950
  logicalResult = group([
1936
1951
  path.call(print, 'left'),
@@ -2814,36 +2829,45 @@ function printArrowFunction(node, path, options, print, args) {
2814
2829
  parts.push(': ', path.call(print, 'returnType'));
2815
2830
  }
2816
2831
 
2817
- parts.push(' => ');
2818
-
2819
2832
  // For block statements, print the body directly to get proper formatting
2820
2833
  if (node.body.type === 'BlockStatement') {
2834
+ parts.push(' => ');
2821
2835
  parts.push(path.call(print, 'body'));
2822
2836
  } else {
2823
2837
  // For expression bodies, check if we need to wrap in parens
2824
2838
  // Wrap ObjectExpression, AssignmentExpression, and SequenceExpression in parens
2825
2839
  // to avoid ambiguity with block statements or to clarify intent
2826
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;
2827
2845
  if (
2828
2846
  node.body.type === 'ObjectExpression' ||
2829
2847
  node.body.type === 'AssignmentExpression' ||
2830
2848
  node.body.type === 'SequenceExpression' ||
2831
2849
  (args?.isInAttribute && isTemplateExpression(node.body))
2832
2850
  ) {
2833
- parts.push('(');
2834
2851
  if (isTemplateExpression(node.body)) {
2835
- parts.push(indent([hardline, bodyDoc]));
2836
- parts.push(hardline);
2852
+ bodyContent = ['(', indent([hardline, bodyDoc]), hardline, ')'];
2837
2853
  } else {
2838
- parts.push(bodyDoc);
2854
+ bodyContent = ['(', bodyDoc, ')'];
2839
2855
  }
2840
- parts.push(')');
2841
2856
  } else {
2842
- parts.push(bodyDoc);
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
+ );
2843
2867
  }
2844
2868
  }
2845
2869
 
2846
- return parts;
2870
+ return group(parts);
2847
2871
  }
2848
2872
 
2849
2873
  /**
@@ -3053,6 +3077,33 @@ function shouldHugArrowFunctions(args) {
3053
3077
  return firstBlockIndex === 0;
3054
3078
  }
3055
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
+
3056
3107
  /**
3057
3108
  * Print call expression arguments
3058
3109
  * @param {AstPath<AST.CallExpression>} path - The call path
package/src/index.test.js CHANGED
@@ -1380,6 +1380,19 @@ import { effect, track } from 'ripple';`;
1380
1380
  expect(result).toBeWithNewline(expected);
1381
1381
  });
1382
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
+
1383
1396
  it('does not add spaces around inlined array elements in destructured arguments', async () => {
1384
1397
  const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
1385
1398
  }