@ripple-ts/prettier-plugin 0.2.159 → 0.2.160

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": "@ripple-ts/prettier-plugin",
3
- "version": "0.2.159",
3
+ "version": "0.2.160",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "prettier": "^3.6.2",
28
- "ripple": "0.2.159"
28
+ "ripple": "0.2.160"
29
29
  },
30
30
  "dependencies": {},
31
31
  "files": [
package/src/index.js CHANGED
@@ -156,6 +156,79 @@ function iterateFunctionParametersPath(path, iteratee) {
156
156
  }
157
157
  }
158
158
 
159
+ // Operator precedence (higher number = higher precedence)
160
+ const PRECEDENCE = {
161
+ '||': 1,
162
+ '&&': 2,
163
+ '|': 3,
164
+ '^': 4,
165
+ '&': 5,
166
+ '==': 6,
167
+ '!=': 6,
168
+ '===': 6,
169
+ '!==': 6,
170
+ '<': 7,
171
+ '<=': 7,
172
+ '>': 7,
173
+ '>=': 7,
174
+ in: 7,
175
+ instanceof: 7,
176
+ '<<': 8,
177
+ '>>': 8,
178
+ '>>>': 8,
179
+ '+': 9,
180
+ '-': 9,
181
+ '*': 10,
182
+ '/': 10,
183
+ '%': 10,
184
+ '**': 11,
185
+ };
186
+
187
+ function getPrecedence(operator) {
188
+ return PRECEDENCE[operator] || 0;
189
+ }
190
+
191
+ // Check if a BinaryExpression needs parentheses
192
+ function binaryExpressionNeedsParens(node, parent) {
193
+ if (!node.metadata?.parenthesized) {
194
+ return false;
195
+ }
196
+
197
+ // If parent is not an operator context, don't preserve parens
198
+ if (
199
+ !parent ||
200
+ (parent.type !== 'BinaryExpression' &&
201
+ parent.type !== 'LogicalExpression' &&
202
+ parent.type !== 'UnaryExpression')
203
+ ) {
204
+ return false;
205
+ }
206
+
207
+ // If parent is UnaryExpression, it already handles the parentheses
208
+ if (parent.type === 'UnaryExpression') {
209
+ return false;
210
+ }
211
+
212
+ // For BinaryExpression/LogicalExpression parents, check precedence
213
+ if (parent.type === 'BinaryExpression' || parent.type === 'LogicalExpression') {
214
+ const nodePrecedence = getPrecedence(node.operator);
215
+ const parentPrecedence = getPrecedence(parent.operator);
216
+
217
+ // Need parens if:
218
+ // 1. Child has lower precedence than parent
219
+ // 2. Same precedence but different operators (for clarity)
220
+ // 3. Child is on the right side and precedence is equal (for left-associative operators)
221
+ if (nodePrecedence < parentPrecedence) {
222
+ return true;
223
+ }
224
+ if (nodePrecedence === parentPrecedence && node.operator !== parent.operator) {
225
+ return true;
226
+ }
227
+ }
228
+
229
+ return false;
230
+ }
231
+
159
232
  function createSkip(characters) {
160
233
  return (text, startIndex, options) => {
161
234
  const backwards = Boolean(options && options.backwards);
@@ -1445,9 +1518,10 @@ function printRippleNode(node, path, options, print, args) {
1445
1518
  parent.type === 'AssignmentExpression' ||
1446
1519
  parent.type === 'AssignmentPattern');
1447
1520
 
1521
+ let result;
1448
1522
  // Don't add indent if we're in a conditional test context
1449
1523
  if (args?.isConditionalTest) {
1450
- nodeContent = group(
1524
+ result = group(
1451
1525
  concat([
1452
1526
  path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
1453
1527
  ' ',
@@ -1460,7 +1534,7 @@ function printRippleNode(node, path, options, print, args) {
1460
1534
  );
1461
1535
  } else if (shouldNotIndent) {
1462
1536
  // In assignment context, don't add indent - parent will handle it
1463
- nodeContent = group(
1537
+ result = group(
1464
1538
  concat([
1465
1539
  path.call(print, 'left'),
1466
1540
  ' ',
@@ -1469,7 +1543,7 @@ function printRippleNode(node, path, options, print, args) {
1469
1543
  ]),
1470
1544
  );
1471
1545
  } else {
1472
- nodeContent = group(
1546
+ result = group(
1473
1547
  concat([
1474
1548
  path.call(print, 'left'),
1475
1549
  ' ',
@@ -1478,6 +1552,13 @@ function printRippleNode(node, path, options, print, args) {
1478
1552
  ]),
1479
1553
  );
1480
1554
  }
1555
+
1556
+ // Wrap in parentheses only if semantically necessary
1557
+ if (binaryExpressionNeedsParens(node, parent)) {
1558
+ result = concat(['(', result, ')']);
1559
+ }
1560
+
1561
+ nodeContent = result;
1481
1562
  break;
1482
1563
  }
1483
1564
  case 'LogicalExpression':
@@ -2239,8 +2320,13 @@ function printArrowFunction(node, path, options, print) {
2239
2320
  parts.push(path.call(print, 'body'));
2240
2321
  } else {
2241
2322
  // For expression bodies, check if we need to wrap in parens
2242
- // Wrap ObjectExpression in parens to avoid ambiguity with block statements
2243
- if (node.body.type === 'ObjectExpression') {
2323
+ // Wrap ObjectExpression, AssignmentExpression, and SequenceExpression in parens
2324
+ // to avoid ambiguity with block statements or to clarify intent
2325
+ if (
2326
+ node.body.type === 'ObjectExpression' ||
2327
+ node.body.type === 'AssignmentExpression' ||
2328
+ node.body.type === 'SequenceExpression'
2329
+ ) {
2244
2330
  parts.push('(');
2245
2331
  parts.push(path.call(print, 'body'));
2246
2332
  parts.push(')');
@@ -3064,7 +3150,13 @@ function printMemberExpression(node, path, options, print) {
3064
3150
 
3065
3151
  let result;
3066
3152
  if (node.computed) {
3067
- const openBracket = node.optional ? '?.[' : '[';
3153
+ // Check if the MemberExpression itself is tracked to add @ symbol
3154
+ const trackedPrefix = node.tracked ? '@' : '';
3155
+ const openBracket = node.optional
3156
+ ? '?.' + trackedPrefix + '['
3157
+ : trackedPrefix
3158
+ ? '.' + trackedPrefix + '['
3159
+ : '[';
3068
3160
  result = concat([objectPart, openBracket, propertyPart, ']']);
3069
3161
  } else {
3070
3162
  const separator = node.optional ? '?.' : '.';
@@ -3095,9 +3187,21 @@ function printUnaryExpression(node, path, options, print) {
3095
3187
  if (needsSpace) {
3096
3188
  parts.push(' ');
3097
3189
  }
3098
- parts.push(path.call(print, 'argument'));
3190
+ const argumentDoc = path.call(print, 'argument');
3191
+ // Preserve parentheses around the argument when present
3192
+ if (node.argument.metadata?.parenthesized) {
3193
+ parts.push('(', argumentDoc, ')');
3194
+ } else {
3195
+ parts.push(argumentDoc);
3196
+ }
3099
3197
  } else {
3100
- parts.push(path.call(print, 'argument'));
3198
+ const argumentDoc = path.call(print, 'argument');
3199
+ // Preserve parentheses around the argument when present
3200
+ if (node.argument.metadata?.parenthesized) {
3201
+ parts.push('(', argumentDoc, ')');
3202
+ } else {
3203
+ parts.push(argumentDoc);
3204
+ }
3101
3205
  parts.push(node.operator);
3102
3206
  }
3103
3207
 
@@ -4637,17 +4741,21 @@ function printJSXMemberExpression(node) {
4637
4741
 
4638
4742
  function printMemberExpressionSimple(node, options, computed = false) {
4639
4743
  if (node.type === 'Identifier') {
4640
- return node.name;
4744
+ // When computed is true, it means we're inside brackets and tracked is already handled by .@[ or [
4745
+ // So we should NOT add @ prefix in that case
4746
+ return (computed ? '' : node.tracked ? '@' : '') + node.name;
4641
4747
  }
4642
4748
 
4643
4749
  if (node.type === 'MemberExpression') {
4644
4750
  const obj = printMemberExpressionSimple(node.object, options);
4751
+ // For properties, we add the .@ or . prefix, and then pass true to indicate
4752
+ // that we're in a context where tracked has been handled
4645
4753
  const prop = node.computed
4646
4754
  ? (node.property.tracked ? '.@[' : '[') +
4647
- printMemberExpressionSimple(node.property, options, node.computed) +
4755
+ printMemberExpressionSimple(node.property, options, true) +
4648
4756
  ']'
4649
4757
  : (node.property.tracked ? '.@' : '.') +
4650
- printMemberExpressionSimple(node.property, options, node.computed);
4758
+ printMemberExpressionSimple(node.property, options, true);
4651
4759
  return obj + prop;
4652
4760
  }
4653
4761
 
@@ -4658,7 +4766,7 @@ function printMemberExpressionSimple(node, options, computed = false) {
4658
4766
  }
4659
4767
 
4660
4768
  function printElement(node, path, options, print) {
4661
- const tagName = (node.id.tracked ? '@' : '') + printMemberExpressionSimple(node.id, options);
4769
+ const tagName = printMemberExpressionSimple(node.id, options);
4662
4770
 
4663
4771
  const elementLeadingComments = getElementLeadingComments(node);
4664
4772
  const metadataCommentParts =
package/src/index.test.js CHANGED
@@ -902,6 +902,44 @@ export component Test({ a, b }: Props) {}`;
902
902
  expect(result).toBeWithNewline(expected);
903
903
  });
904
904
 
905
+ it('should not strip @ from dynamic self-closing components', async () => {
906
+ const expected = `component App() {
907
+ <@tracked_object.@tracked_basic />
908
+ }`;
909
+
910
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
911
+ expect(result).toBeWithNewline(expected);
912
+ });
913
+
914
+ it('should keep @ on dynamic object member array expressions', async () => {
915
+ const expected = `component App() {
916
+ const obj = {
917
+ [0]: track(0),
918
+ };
919
+
920
+ <div>{obj.@[0]}</div>
921
+
922
+ <button
923
+ onClick={() => {
924
+ obj.@[0]++;
925
+ }}
926
+ >
927
+ {'Increment'}
928
+ </button>
929
+
930
+ <button
931
+ onClick={() => {
932
+ obj.@[0] += 1;
933
+ }}
934
+ >
935
+ {'Increment'}
936
+ </button>
937
+ }`;
938
+
939
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
940
+ expect(result).toBeWithNewline(expected);
941
+ });
942
+
905
943
  it('keeps a new line between comments above and code if one is present', async () => {
906
944
  const expected = `// comment
907
945
 
@@ -1105,6 +1143,30 @@ const set = new #Set([1, 2, 3]);`;
1105
1143
  expect(result).toBeWithNewline(expected);
1106
1144
  });
1107
1145
 
1146
+ it('should keep TrackedSet parents with short syntax and no args intact', async () => {
1147
+ const expected = `component SetTest() {
1148
+ let items = new #Set();
1149
+
1150
+ <button onClick={() => items.add(1)}>{'add'}</button>
1151
+ <pre>{items.size}</pre>
1152
+ }`;
1153
+
1154
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1155
+ expect(result).toBeWithNewline(expected);
1156
+ });
1157
+
1158
+ it('should keep TrackedMap parents with short syntax and no args intact', async () => {
1159
+ const expected = `component MapTest() {
1160
+ let items = new #Map();
1161
+
1162
+ <button onClick={() => items.set('key', 1)}>{'add'}</button>
1163
+ <pre>{items.size}</pre>
1164
+ }`;
1165
+
1166
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1167
+ expect(result).toBeWithNewline(expected);
1168
+ });
1169
+
1108
1170
  it('should not remove blank lines between components and types if provided', async () => {
1109
1171
  const expected = `export component App() {
1110
1172
  console.log('test');
@@ -1496,6 +1558,59 @@ const program =
1496
1558
  const result = await format(input, { singleQuote: true, printWidth: 30 });
1497
1559
  expect(result).toBeWithNewline(expected);
1498
1560
  });
1561
+
1562
+ it('should keep blank lines between commented out block and markup', async () => {
1563
+ const expected = `function CounterWrapper(props) {
1564
+ const more = {
1565
+ double: track(() => props.count * 2),
1566
+ another: track(0),
1567
+ onemore: 100,
1568
+ };
1569
+
1570
+ // if (props.@count > 1) {
1571
+ // delete more.another;
1572
+ // }
1573
+
1574
+ <div>
1575
+ <Counter {...props} {...more} />
1576
+ </div>
1577
+ }`;
1578
+
1579
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1580
+ expect(result).toBeWithNewline(expected);
1581
+ });
1582
+
1583
+ it('should keep parens around negating key in object expression', async () => {
1584
+ const input = `effect(() => {
1585
+ props.count;
1586
+ if (props.count > 1 && 'another' in more) {
1587
+ untrack(() => delete more.another);
1588
+ } else if (props.count > 2 && !('another' in more)) {
1589
+ untrack(() => more.another = 0);
1590
+ }
1591
+ untrack(() => console.log(more));
1592
+ });`;
1593
+
1594
+ const expected = `effect(() => {
1595
+ props.count;
1596
+ if (props.count > 1 && 'another' in more) {
1597
+ untrack(() => delete more.another);
1598
+ } else if (props.count > 2 && !('another' in more)) {
1599
+ untrack(() => (more.another = 0));
1600
+ }
1601
+ untrack(() => console.log(more));
1602
+ });`;
1603
+
1604
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
1605
+ expect(result).toBeWithNewline(expected);
1606
+ });
1607
+
1608
+ it('should keep parents in math subtraction and multiplication', async () => {
1609
+ const expected = `let offset = track(() => (@page - 1) * @limit);`;
1610
+
1611
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1612
+ expect(result).toBeWithNewline(expected);
1613
+ });
1499
1614
  });
1500
1615
 
1501
1616
  describe('edge cases', () => {