@ripple-ts/prettier-plugin 0.2.214 → 0.2.215

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.214",
3
+ "version": "0.2.215",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -26,8 +26,8 @@
26
26
  "devDependencies": {
27
27
  "@types/estree": "^1.0.8",
28
28
  "@types/estree-jsx": "^1.0.5",
29
- "prettier": "^3.6.2",
30
- "ripple": "0.2.214"
29
+ "prettier": "^3.8.1",
30
+ "ripple": "0.2.215"
31
31
  },
32
32
  "dependencies": {},
33
33
  "files": [
package/src/index.js CHANGED
@@ -1505,6 +1505,11 @@ function printRippleNode(node, path, options, print, args) {
1505
1505
  nodeContent = printMemberExpression(node, path, options, print);
1506
1506
  break;
1507
1507
 
1508
+ case 'MetaProperty':
1509
+ // Prints import.meta, new.target, etc.
1510
+ nodeContent = [path.call(print, 'meta'), '.', path.call(print, 'property')];
1511
+ break;
1512
+
1508
1513
  case 'Super':
1509
1514
  nodeContent = 'super';
1510
1515
  break;
@@ -1917,7 +1922,25 @@ function printRippleNode(node, path, options, print, args) {
1917
1922
  break;
1918
1923
  }
1919
1924
  }
1920
- nodeContent = '{}';
1925
+
1926
+ // Control flow statements (if, for, while, etc.) get expanded empty blocks
1927
+ // to match standard Prettier behavior. Functions/methods keep `{}`.
1928
+ const blockParent = path.getParentNode();
1929
+ const isControlFlow =
1930
+ blockParent &&
1931
+ (blockParent.type === 'IfStatement' ||
1932
+ blockParent.type === 'ForStatement' ||
1933
+ blockParent.type === 'ForInStatement' ||
1934
+ blockParent.type === 'ForOfStatement' ||
1935
+ blockParent.type === 'WhileStatement' ||
1936
+ blockParent.type === 'DoWhileStatement' ||
1937
+ blockParent.type === 'SwitchCase');
1938
+
1939
+ if (isControlFlow) {
1940
+ nodeContent = ['{', hardline, '}'];
1941
+ } else {
1942
+ nodeContent = '{}';
1943
+ }
1921
1944
  break;
1922
1945
  }
1923
1946
 
@@ -2007,24 +2030,34 @@ function printRippleNode(node, path, options, print, args) {
2007
2030
  nodeContent = result;
2008
2031
  break;
2009
2032
  }
2010
- case 'LogicalExpression':
2033
+ case 'LogicalExpression': {
2034
+ const logicalParent = path.getParentNode();
2035
+ let logicalResult;
2011
2036
  // Don't add indent if we're in a conditional test context
2012
2037
  if (args?.isConditionalTest) {
2013
- nodeContent = group([
2038
+ logicalResult = group([
2014
2039
  path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
2015
2040
  ' ',
2016
2041
  node.operator,
2017
2042
  [line, path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right')],
2018
2043
  ]);
2019
2044
  } else {
2020
- nodeContent = group([
2045
+ logicalResult = group([
2021
2046
  path.call(print, 'left'),
2022
2047
  ' ',
2023
2048
  node.operator,
2024
2049
  indent([line, path.call(print, 'right')]),
2025
2050
  ]);
2026
2051
  }
2052
+
2053
+ // Wrap in parentheses only if semantically necessary
2054
+ if (binaryExpressionNeedsParens(node, logicalParent)) {
2055
+ logicalResult = ['(', logicalResult, ')'];
2056
+ }
2057
+
2058
+ nodeContent = logicalResult;
2027
2059
  break;
2060
+ }
2028
2061
 
2029
2062
  case 'ConditionalExpression': {
2030
2063
  // Use Prettier's grouping to handle line breaking when exceeding printWidth
@@ -3640,6 +3673,7 @@ function printDoWhileStatement(node, path, options, print) {
3640
3673
  parts.push('while (');
3641
3674
  parts.push(test);
3642
3675
  parts.push(')');
3676
+ parts.push(semi(options));
3643
3677
 
3644
3678
  return parts;
3645
3679
  }
@@ -5923,6 +5957,53 @@ function printElement(node, path, options, print) {
5923
5957
  const isSelfClosing = !!node.selfClosing;
5924
5958
  const hasAttributes = Array.isArray(node.attributes) && node.attributes.length > 0;
5925
5959
 
5960
+ // Collect comments that the parser attached to children but actually belong inside
5961
+ // the opening tag (positionally before openingElement.end). These should be printed
5962
+ // as leading comments before the appropriate attribute, not lifted to element-level.
5963
+ const openingTagCommentsSet = new Set();
5964
+ if (hasChildren && node.openingElement) {
5965
+ const openingEnd = node.openingElement.end;
5966
+ for (const child of node.children) {
5967
+ if (
5968
+ (child.type === 'Text' || child.type === 'Html') &&
5969
+ Array.isArray(child.leadingComments)
5970
+ ) {
5971
+ for (const comment of child.leadingComments) {
5972
+ if (
5973
+ typeof comment.start === 'number' &&
5974
+ comment.start >= node.start &&
5975
+ comment.start < openingEnd
5976
+ ) {
5977
+ openingTagCommentsSet.add(comment);
5978
+ }
5979
+ }
5980
+ }
5981
+ }
5982
+ }
5983
+
5984
+ // Build a map from attribute index to comments that should precede that attribute
5985
+ const openingTagCommentsByAttrIndex = new Map();
5986
+ if (openingTagCommentsSet.size > 0 && hasAttributes) {
5987
+ const sortedOTC = [...openingTagCommentsSet].sort(
5988
+ (a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start),
5989
+ );
5990
+ let commentIdx = 0;
5991
+ for (let attrIdx = 0; attrIdx < node.attributes.length; attrIdx++) {
5992
+ const attr = node.attributes[attrIdx];
5993
+ const commentsForAttr = [];
5994
+ while (
5995
+ commentIdx < sortedOTC.length &&
5996
+ /** @type {number} */ (sortedOTC[commentIdx].start) < attr.start
5997
+ ) {
5998
+ commentsForAttr.push(sortedOTC[commentIdx]);
5999
+ commentIdx++;
6000
+ }
6001
+ if (commentsForAttr.length > 0) {
6002
+ openingTagCommentsByAttrIndex.set(attrIdx, commentsForAttr);
6003
+ }
6004
+ }
6005
+ }
6006
+
5926
6007
  if (isSelfClosing && !hasInnerComments && !hasAttributes) {
5927
6008
  const elementDoc = group(['<', tagName, ' />']);
5928
6009
  return metadataCommentParts.length > 0 ? [...metadataCommentParts, elementDoc] : elementDoc;
@@ -5935,14 +6016,36 @@ function printElement(node, path, options, print) {
5935
6016
 
5936
6017
  const shouldUseSelfClosingSyntax = isSelfClosing || (!hasChildren && !hasInnerComments);
5937
6018
 
6019
+ const hasOpeningTagComments = openingTagCommentsSet.size > 0;
6020
+ let attrIndex = 0;
5938
6021
  const openingTag = group([
5939
6022
  '<',
5940
6023
  tagName,
5941
6024
  hasAttributes
5942
6025
  ? indent([
5943
6026
  ...path.map((attrPath) => {
5944
- return [attrLineBreak, print(attrPath)];
6027
+ const idx = attrIndex++;
6028
+ const commentsForAttr = openingTagCommentsByAttrIndex.get(idx);
6029
+ const parts = [];
6030
+ if (commentsForAttr) {
6031
+ for (const comment of commentsForAttr) {
6032
+ // Line comments (//) consume the rest of the line, so they must
6033
+ // use hardline to force a break. Block comments can use normal breaks.
6034
+ if (comment.type === 'Line') {
6035
+ parts.push(hardline);
6036
+ parts.push('//' + comment.value);
6037
+ } else if (comment.type === 'Block') {
6038
+ parts.push(attrLineBreak);
6039
+ parts.push('/*' + comment.value + '*/');
6040
+ }
6041
+ }
6042
+ }
6043
+ parts.push(attrLineBreak);
6044
+ parts.push(print(attrPath));
6045
+ return parts;
5945
6046
  }, 'attributes'),
6047
+ // Force the group to break when there are line comments in the opening tag
6048
+ ...(hasOpeningTagComments ? [breakParent] : []),
5946
6049
  ])
5947
6050
  : '',
5948
6051
  // Add line break opportunity before > or />
@@ -6045,7 +6148,11 @@ function printElement(node, path, options, print) {
6045
6148
 
6046
6149
  if (hasTextLeadingComments) {
6047
6150
  for (let j = 0; j < currentChild.leadingComments.length; j++) {
6048
- fallbackElementComments.push(currentChild.leadingComments[j]);
6151
+ const comment = currentChild.leadingComments[j];
6152
+ // Don't lift comments that belong inside the opening tag (handled in attribute section)
6153
+ if (!openingTagCommentsSet.has(comment)) {
6154
+ fallbackElementComments.push(comment);
6155
+ }
6049
6156
  }
6050
6157
  }
6051
6158
 
package/src/index.test.js CHANGED
@@ -269,6 +269,17 @@ export default component App() {
269
269
  expect(formatted).toBeWithNewline(already_formatted);
270
270
  });
271
271
 
272
+ it('should format import.meta expressions correctly', async () => {
273
+ const input = `export component Test(){if(import.meta.env.SSR){<div>{'Server'}</div>}}`;
274
+ const expected = `export component Test() {
275
+ if (import.meta.env.SSR) {
276
+ <div>{'Server'}</div>
277
+ }
278
+ }`;
279
+ const result = await format(input, { singleQuote: true });
280
+ expect(result).toBeWithNewline(expected);
281
+ });
282
+
272
283
  it('should format a component with an object property notation component markup', async () => {
273
284
  const expected = `component Card(props) {
274
285
  <div class="card">
@@ -1017,7 +1028,8 @@ import { effect, track } from 'ripple';`;
1017
1028
  });
1018
1029
 
1019
1030
  it('does not add spaces around inlined array elements in destructured arguments', async () => {
1020
- const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {}
1031
+ const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
1032
+ }
1021
1033
  const [obj1, obj2] = arrayOfObjects;`;
1022
1034
  const result = await format(expected, { singleQuote: true, printWidth: 100 });
1023
1035
  expect(result).toBeWithNewline(expected);
@@ -2101,6 +2113,102 @@ files = [...(files ?? []), ...dt.files];`;
2101
2113
  const result = await format(expected, { singleQuote: true, printWidth: 100 });
2102
2114
  expect(result).toBeWithNewline(expected);
2103
2115
  });
2116
+
2117
+ it('should preserve comments above attributes on dom elements', async () => {
2118
+ const expected = `component App() {
2119
+ <div
2120
+ // @ripple-ignore
2121
+ something="test"
2122
+ >
2123
+ {'test'}
2124
+ </div>
2125
+ }`;
2126
+
2127
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2128
+ expect(result).toBeWithNewline(expected);
2129
+ });
2130
+
2131
+ it('should preserve comments above attributes on components', async () => {
2132
+ const expected = `component App() {
2133
+ <Child
2134
+ // @ripple-ignore
2135
+ something="test"
2136
+ >
2137
+ {'test'}
2138
+ </Child>
2139
+ }
2140
+ component Child({ something }) {
2141
+ <div>{something}</div>
2142
+ }`;
2143
+
2144
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2145
+ expect(result).toBeWithNewline(expected);
2146
+ });
2147
+
2148
+ it('keeps parens in place when necessary for logical reasons with && and || operators', async () => {
2149
+ const expected = `function App() {
2150
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2151
+ }
2152
+ }`;
2153
+
2154
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2155
+ expect(result).toBeWithNewline(expected);
2156
+ });
2157
+
2158
+ it('expands empty braces to new lines for for-in statements', async () => {
2159
+ const expected = `for (const key in obj) {
2160
+ }`;
2161
+ const result = await format(expected);
2162
+ expect(result).toBeWithNewline(expected);
2163
+ });
2164
+
2165
+ it('expands empty braces to new lines for for statements', async () => {
2166
+ const expected = `for (let i = 0; i < 10; i++) {
2167
+ }`;
2168
+ const result = await format(expected);
2169
+ expect(result).toBeWithNewline(expected);
2170
+ });
2171
+
2172
+ it('expands empty braces to new lines for while statements', async () => {
2173
+ const expected = `while (true) {
2174
+ }`;
2175
+ const result = await format(expected);
2176
+ expect(result).toBeWithNewline(expected);
2177
+ });
2178
+
2179
+ it('expands empty braces to new lines for do-while statements', async () => {
2180
+ const expected = `do {
2181
+ } while (true);`;
2182
+ const result = await format(expected);
2183
+ expect(result).toBeWithNewline(expected);
2184
+ });
2185
+
2186
+ it('adds semicolon after do-while when semi option is true', async () => {
2187
+ const input = `do { console.log('x') } while (true)`;
2188
+ const expected = `do {
2189
+ console.log("x");
2190
+ } while (true);`;
2191
+ const result = await format(input);
2192
+ expect(result).toBeWithNewline(expected);
2193
+ });
2194
+
2195
+ it('omits semicolon after do-while when semi option is false', async () => {
2196
+ const input = `do { console.log('x') } while (true);`;
2197
+ const expected = `do {
2198
+ console.log("x")
2199
+ } while (true)`;
2200
+ const result = await format(input, { semi: false });
2201
+ expect(result).toBeWithNewline(expected);
2202
+ });
2203
+
2204
+ it('expands empty braces to new lines for switch case blocks', async () => {
2205
+ const expected = `switch (x) {
2206
+ case 1: {
2207
+ }
2208
+ }`;
2209
+ const result = await format(expected);
2210
+ expect(result).toBeWithNewline(expected);
2211
+ });
2104
2212
  });
2105
2213
 
2106
2214
  describe('edge cases', () => {
@@ -2767,8 +2875,10 @@ const obj2 = #{
2767
2875
  const input = `for (const [i = 0, item] of items.entries()) {}
2768
2876
  for (const {i = 0, item} of items.entries()) {}`;
2769
2877
 
2770
- const expected = `for (const [i = 0, item] of items.entries()) {}
2771
- for (const { i = 0, item } of items.entries()) {}`;
2878
+ const expected = `for (const [i = 0, item] of items.entries()) {
2879
+ }
2880
+ for (const { i = 0, item } of items.entries()) {
2881
+ }`;
2772
2882
 
2773
2883
  const result = await format(input);
2774
2884
  expect(result).toBeWithNewline(expected);