@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsrx/prettier-plugin",
3
- "version": "0.3.62",
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.11"
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 the element, not as part of the expression
767
+ // as separate children within elements, not as part of the expression.
724
768
  const shouldSkipLeadingComments =
725
- node.type === 'TSRXExpression' || node.type === 'Text' || node.type === 'Html';
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
- parts.push(indent([hardline, bodyDoc]));
2787
- parts.push(hardline);
2852
+ bodyContent = ['(', indent([hardline, bodyDoc]), hardline, ')'];
2788
2853
  } else {
2789
- parts.push(bodyDoc);
2854
+ bodyContent = ['(', bodyDoc, ')'];
2790
2855
  }
2791
- parts.push(')');
2792
2856
  } else {
2793
- 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
+ );
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
  }