@tsrx/prettier-plugin 0.3.74 → 0.3.77

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.74",
3
+ "version": "0.3.77",
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.22"
30
+ "@tsrx/core": "0.1.25"
31
31
  },
32
32
  "files": [
33
33
  "src/"
package/src/index.js CHANGED
@@ -42,16 +42,16 @@ const { replaceEndOfLine, willBreak } = utils;
42
42
  /** @type {import('prettier').Plugin['languages']} */
43
43
  export const languages = [
44
44
  {
45
- name: 'ripple',
46
- parsers: ['ripple'],
45
+ name: 'tsrx',
46
+ parsers: ['tsrx'],
47
47
  extensions: ['.tsrx'],
48
- vscodeLanguageIds: ['ripple'],
48
+ vscodeLanguageIds: ['tsrx', 'ripple'],
49
49
  },
50
50
  ];
51
51
 
52
52
  /** @type {import('prettier').Plugin['parsers']} */
53
53
  export const parsers = {
54
- ripple: {
54
+ tsrx: {
55
55
  astFormat: 'ripple-ast',
56
56
  /**
57
57
  * @param {string} text
@@ -1732,18 +1732,12 @@ function printRippleNode(node, path, options, print, args) {
1732
1732
  }
1733
1733
  case 'Identifier': {
1734
1734
  // Simple case - just return the name directly like Prettier core
1735
- const trackedPrefix = node.tracked ? '@' : '';
1736
1735
  let identifierContent;
1737
1736
  if (node.typeAnnotation) {
1738
1737
  const optionalMarker = node.optional ? '?' : '';
1739
- identifierContent = [
1740
- trackedPrefix + node.name,
1741
- optionalMarker,
1742
- ': ',
1743
- path.call(print, 'typeAnnotation'),
1744
- ];
1738
+ identifierContent = [node.name, optionalMarker, ': ', path.call(print, 'typeAnnotation')];
1745
1739
  } else {
1746
- identifierContent = trackedPrefix + node.name;
1740
+ identifierContent = node.name;
1747
1741
  }
1748
1742
  // Preserve parentheses for type-cast identifiers, but only if:
1749
1743
  // 1. The identifier itself is marked as parenthesized
@@ -1857,7 +1851,13 @@ function printRippleNode(node, path, options, print, args) {
1857
1851
  blockParent.type === 'ForOfStatement' ||
1858
1852
  blockParent.type === 'WhileStatement' ||
1859
1853
  blockParent.type === 'DoWhileStatement' ||
1860
- blockParent.type === 'SwitchCase');
1854
+ blockParent.type === 'TryStatement' ||
1855
+ blockParent.type === 'CatchClause' ||
1856
+ blockParent.type === 'SwitchCase' ||
1857
+ blockParent.type === 'JSXIfExpression' ||
1858
+ blockParent.type === 'JSXForExpression' ||
1859
+ blockParent.type === 'JSXTryExpression' ||
1860
+ blockParent.type === 'JSXSwitchExpression');
1861
1861
 
1862
1862
  if (isControlFlow) {
1863
1863
  nodeContent = ['{', hardline, '}'];
@@ -4010,7 +4010,6 @@ function printMemberExpression(node, path, options, print) {
4010
4010
 
4011
4011
  let result;
4012
4012
  if (node.computed) {
4013
- // Check if the MemberExpression itself is tracked to add @ symbol
4014
4013
  const openBracket = node.optional ? '?.[' : '[';
4015
4014
  result = [objectPart, openBracket, propertyPart, ']'];
4016
4015
  } else {
@@ -6086,11 +6085,19 @@ function printJSXElement(node, path, options, print) {
6086
6085
  return Array.isArray(leadingComments) && leadingComments.length > 0;
6087
6086
  });
6088
6087
  const forceMultiline = hasClosingComments || hasChildLeadingComments;
6088
+ const singleChildNode = childNodes.length === 1 ? childNodes[0] : null;
6089
+ const hasAuthoredMultilineSingleTextChild =
6090
+ singleChildNode?.type === 'JSXText' && /[\r\n]/u.test(singleChildNode.value);
6089
6091
 
6090
6092
  // Check if content can be inlined (single text node or single expression).
6091
6093
  // Trailing or child-leading comments force the multi-line layout. A single
6092
6094
  // text child stays inline when it fits and otherwise fills/wraps to printWidth.
6093
- if (!forceMultiline && childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
6095
+ if (
6096
+ !forceMultiline &&
6097
+ !hasAuthoredMultilineSingleTextChild &&
6098
+ childrenDocs.length === 1 &&
6099
+ typeof childrenDocs[0] === 'string'
6100
+ ) {
6094
6101
  // The open tag breaks for attributes independently; the text+closing get
6095
6102
  // their own group so the text only drops to its own (filled) lines when it
6096
6103
  // itself overflows — otherwise it hugs `>text</tag>`.
@@ -6464,7 +6471,7 @@ function printJSXAttribute(attr, path, options, print) {
6464
6471
  */
6465
6472
  function printJSXElementName(node) {
6466
6473
  if (node.type === 'JSXIdentifier') {
6467
- return (isDynamicJSXIdentifier(node) ? '@' : '') + node.name;
6474
+ return node.name;
6468
6475
  }
6469
6476
  if (node.type === 'JSXMemberExpression') {
6470
6477
  return printJSXElementName(node.object) + '.' + printJSXElementName(node.property);
@@ -6477,14 +6484,6 @@ function printJSXElementName(node) {
6477
6484
  return 'Unknown';
6478
6485
  }
6479
6486
 
6480
- /**
6481
- * @param {ESTreeJSX.JSXIdentifier} node
6482
- * @returns {boolean}
6483
- */
6484
- function isDynamicJSXIdentifier(node) {
6485
- return /** @type {{ dynamic?: boolean }} */ (node).dynamic === true;
6486
- }
6487
-
6488
6487
  /**
6489
6488
  * Print a member expression as simple string (for element tag names)
6490
6489
  * @param {AST.Node} node - The node to print
@@ -6494,7 +6493,7 @@ function isDynamicJSXIdentifier(node) {
6494
6493
  */
6495
6494
  function printMemberExpressionSimple(node, options, computed = false) {
6496
6495
  if (node.type === 'JSXIdentifier') {
6497
- return (isDynamicJSXIdentifier(node) ? '@' : '') + node.name;
6496
+ return node.name;
6498
6497
  }
6499
6498
 
6500
6499
  if (node.type === 'JSXMemberExpression') {
@@ -6510,7 +6509,7 @@ function printMemberExpressionSimple(node, options, computed = false) {
6510
6509
  }
6511
6510
 
6512
6511
  if (node.type === 'Identifier') {
6513
- return (computed ? '' : node.tracked ? '@' : '') + node.name;
6512
+ return node.name;
6514
6513
  }
6515
6514
 
6516
6515
  if (node.type === 'MemberExpression') {
package/src/index.test.js CHANGED
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import prettier from 'prettier';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
- import { languages } from './index.js';
5
+ import { languages, parsers } from './index.js';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
@@ -40,13 +40,16 @@ expect.extend({
40
40
 
41
41
  describe('prettier-plugin', () => {
42
42
  it('registers .tsrx as a supported file extension', () => {
43
- const ripple_language = languages?.[0];
43
+ const tsrx_language = languages?.[0];
44
44
 
45
- if (!ripple_language) {
46
- throw new Error('Missing Ripple language metadata');
45
+ if (!tsrx_language) {
46
+ throw new Error('Missing TSRX language metadata');
47
47
  }
48
48
 
49
- expect(ripple_language.extensions).toContain('.tsrx');
49
+ expect(tsrx_language.extensions).toContain('.tsrx');
50
+ expect(tsrx_language.parsers).toContain('tsrx');
51
+ expect(parsers?.tsrx).toBeDefined();
52
+ expect(parsers?.ripple).toBeUndefined();
50
53
  });
51
54
 
52
55
  /**
@@ -55,7 +58,7 @@ describe('prettier-plugin', () => {
55
58
  */
56
59
  const format = async (code, options = {}) => {
57
60
  return await prettier.format(code, {
58
- parser: 'ripple',
61
+ parser: 'tsrx',
59
62
  plugins: [join(__dirname, 'index.js')],
60
63
  ...options,
61
64
  });
@@ -233,10 +236,10 @@ const items=[1,2,3];
233
236
  });
234
237
 
235
238
  it('keeps native fragments expression based', async () => {
236
- const input = `function App(){return <><div>"Hello world"</div>{value}</>}`;
239
+ const input = `function App(){return <><div>Hello world</div>{value}</>}`;
237
240
  const expected = `function App() {
238
241
  return <>
239
- <div>"Hello world"</div>
242
+ <div>Hello world</div>
240
243
  {value}
241
244
  </>;
242
245
  }`;
@@ -277,17 +280,17 @@ const items=[1,2,3];
277
280
  const input = `export function App() {
278
281
  let [count] = track(0);
279
282
  return <div>
280
- <p>"Count: "{count}</p>
281
- <p>"Count: "{count}</p>
282
- <button onClick={() => count++}>"Increment"</button>
283
+ <p>Count: {count}</p>
284
+ <p>Count: {count}</p>
285
+ <button onClick={() => count++}>Increment</button>
283
286
  </div>;
284
287
  }`;
285
288
  const expected = `export function App() {
286
289
  let [count] = track(0);
287
290
  return <div>
288
- <p>"Count: "{count}</p>
289
- <p>"Count: "{count}</p>
290
- <button onClick={() => count++}>"Increment"</button>
291
+ <p>Count: {count}</p>
292
+ <p>Count: {count}</p>
293
+ <button onClick={() => count++}>Increment</button>
291
294
  </div>;
292
295
  }`;
293
296
 
@@ -295,6 +298,39 @@ const items=[1,2,3];
295
298
  expect(result).toBeWithNewline(expected);
296
299
  });
297
300
 
301
+ it('preserves authored multiline whitespace around a single JSXText child', async () => {
302
+ const input = `function Foo() @{
303
+ @if (props.onRemove) {
304
+ <button
305
+ class={\`\${styles.actionButton} \${styles.actionButtonDanger}\`}
306
+ type="button"
307
+ onClick={() => {
308
+ void props.onRemove?.();
309
+ }}
310
+ >
311
+ Remove shortcut
312
+ </button>
313
+ }
314
+ }`;
315
+
316
+ const expected = `function Foo() @{
317
+ @if (props.onRemove) {
318
+ <button
319
+ class={\`\${styles.actionButton} \${styles.actionButtonDanger}\`}
320
+ type="button"
321
+ onClick={() => {
322
+ void props.onRemove?.();
323
+ }}
324
+ >
325
+ Remove shortcut
326
+ </button>
327
+ }
328
+ }`;
329
+
330
+ const result = await format(input);
331
+ expect(result).toBeWithNewline(expected);
332
+ });
333
+
298
334
  it('preserves inline text spaces around expression children', async () => {
299
335
  const input = `function Test(){return <div><p class="status">Visible: {String(visible)}</p><p>{name} is visible</p><p>Hello {name}!</p></div>}`;
300
336
  const expected = `function Test() {
@@ -400,28 +436,6 @@ function App() {
400
436
  expect(result).toBeWithNewline(expected);
401
437
  });
402
438
 
403
- it('preserves dynamic component syntax', async () => {
404
- const input = `function App(){return <>@{
405
- function Basic(){return <><div>{"Basic Component"}</div></>;}
406
- const obj={Basic};
407
- const comp=obj.Basic;
408
- <@comp />
409
- }</>}`;
410
- const expected = `function App() {
411
- return <>@{
412
- function Basic() {
413
- return <><div>{"Basic Component"}</div></>;
414
- }
415
- const obj = { Basic };
416
- const comp = obj.Basic;
417
- <@comp />
418
- }</>;
419
- }`;
420
-
421
- const result = await format(input);
422
- expect(result).toBeWithNewline(expected);
423
- });
424
-
425
439
  it('formats raw HTML props inside native elements', async () => {
426
440
  const input = `function App(){return <article innerHTML={source}/>}`;
427
441
  const expected = `function App() {
@@ -608,10 +622,10 @@ const items=[1,2,3];
608
622
  });
609
623
 
610
624
  it('should keep sibling children in tsrx expression fragments on separate lines', async () => {
611
- const input = `function Test(p1,p2){return <><div>"Hello"</div><div>{p1}</div><div>{p2}</div></>}`;
625
+ const input = `function Test(p1,p2){return <><div>Hello</div><div>{p1}</div><div>{p2}</div></>}`;
612
626
  const expected = `function Test(p1, p2) {
613
627
  return <>
614
- <div>"Hello"</div>
628
+ <div>Hello</div>
615
629
  <div>{p1}</div>
616
630
  <div>{p2}</div>
617
631
  </>;
@@ -706,9 +720,9 @@ const items=[1,2,3];
706
720
  });
707
721
 
708
722
  it('keeps fitting single-child fragments inline and expands non-fitting single-child fragments', async () => {
709
- const input = `function Test(){const short=<><span>"Ready"</span></>;const long=<><ReallyLongComponentName first={alpha} second={beta} third={gamma}/></>;}`;
723
+ const input = `function Test(){const short=<><span>Ready</span></>;const long=<><ReallyLongComponentName first={alpha} second={beta} third={gamma}/></>;}`;
710
724
  const expected = `function Test() {
711
- const short = <><span>"Ready"</span></>;
725
+ const short = <><span>Ready</span></>;
712
726
  const long = <>
713
727
  <ReallyLongComponentName
714
728
  first={alpha}
@@ -723,16 +737,16 @@ const items=[1,2,3];
723
737
  });
724
738
 
725
739
  it('expands multi-child fragments while keeping fitting openers on the first line', async () => {
726
- const input = `function Test(){const short=<><div>"A"</div><div>"B"</div></>;const thisNameIsRidiculouslyLongEnoughToMissThePrintWidth=<><div>"A"</div><div>"B"</div></>;}`;
740
+ const input = `function Test(){const short=<><div>A</div><div>B</div></>;const thisNameIsRidiculouslyLongEnoughToMissThePrintWidth=<><div>A</div><div>B</div></>;}`;
727
741
  const expected = `function Test() {
728
742
  const short = <>
729
- <div>"A"</div>
730
- <div>"B"</div>
743
+ <div>A</div>
744
+ <div>B</div>
731
745
  </>;
732
746
  const thisNameIsRidiculouslyLongEnoughToMissThePrintWidth =
733
747
  <>
734
- <div>"A"</div>
735
- <div>"B"</div>
748
+ <div>A</div>
749
+ <div>B</div>
736
750
  </>;
737
751
  }`;
738
752
 
@@ -816,7 +830,7 @@ const items=[1,2,3];
816
830
  */
817
831
  const format = async (code, options = {}) => {
818
832
  return await prettier.format(code, {
819
- parser: 'ripple',
833
+ parser: 'tsrx',
820
834
  plugins: [join(__dirname, 'index.js')],
821
835
  ...options,
822
836
  });
@@ -829,7 +843,7 @@ const items=[1,2,3];
829
843
  */
830
844
  const formatWithCursorHelper = async (code, options) =>
831
845
  await prettier.formatWithCursor(code, {
832
- parser: 'ripple',
846
+ parser: 'tsrx',
833
847
  plugins: [join(__dirname, 'index.js')],
834
848
  ...options,
835
849
  });
@@ -882,13 +896,14 @@ const items=[1,2,3];
882
896
  });
883
897
 
884
898
  it('registers .tsrx as a supported file extension', () => {
885
- const ripple_language = languages?.[0];
899
+ const tsrx_language = languages?.[0];
886
900
 
887
- if (!ripple_language) {
888
- throw new Error('Missing Ripple language metadata');
901
+ if (!tsrx_language) {
902
+ throw new Error('Missing TSRX language metadata');
889
903
  }
890
904
 
891
- expect(ripple_language.extensions).toContain('.tsrx');
905
+ expect(tsrx_language.extensions).toContain('.tsrx');
906
+ expect(tsrx_language.parsers).toContain('tsrx');
892
907
  });
893
908
 
894
909
  it('should format a simple component', async () => {
@@ -966,23 +981,12 @@ const items=[1,2,3];
966
981
  expect(result).toBeWithNewline(expected);
967
982
  });
968
983
 
969
- it('should format a function with a dynamic function using props member access', async () => {
970
- const expected = `function Card(props) {
971
- <div class="card">
972
- <@props.children />
973
- </div>
974
- }`;
975
-
976
- const result = await format(expected, { singleQuote: true });
977
- expect(result).toBeWithNewline(expected);
978
- });
979
-
980
984
  it('should respect print width when using ternary expressions', async () => {
981
985
  const input = `function printMemberExpressionSimple(node, options, computed = false) {
982
986
  if (node.type === 'MemberExpression') {
983
987
  const prop = node.computed
984
- ? (node.property.tracked ? '.@[' : '[') + printMemberExpressionSimple(node.property, options, node.computed) + ']'
985
- : (node.property.tracked ? '.@' : '.') + printMemberExpressionSimple(node.property, options, node.computed);
988
+ ? (node.optional ? '?.[' : '[') + printMemberExpressionSimple(node.property, options, node.computed) + ']'
989
+ : (node.optional ? '?.' : '.') + printMemberExpressionSimple(node.property, options, node.computed);
986
990
  }
987
991
  }`;
988
992
 
@@ -993,14 +997,14 @@ const items=[1,2,3];
993
997
  ) {
994
998
  if (node.type === 'MemberExpression') {
995
999
  const prop = node.computed
996
- ? (node.property.tracked ? '.@[' : '[') +
1000
+ ? (node.optional ? '?.[' : '[') +
997
1001
  printMemberExpressionSimple(
998
1002
  node.property,
999
1003
  options,
1000
1004
  node.computed,
1001
1005
  ) +
1002
1006
  ']'
1003
- : (node.property.tracked ? '.@' : '.') +
1007
+ : (node.optional ? '?.' : '.') +
1004
1008
  printMemberExpressionSimple(
1005
1009
  node.property,
1006
1010
  options,
@@ -1398,11 +1402,11 @@ export function Test({ a, b }: Props) {}`;
1398
1402
 
1399
1403
  it('should not force attribute-less elements to break with singleAttributePerLine', async () => {
1400
1404
  const input = `function One() @{
1401
- <div>"Hello"</div>
1405
+ <div>Hello</div>
1402
1406
  }`;
1403
1407
 
1404
1408
  const expected = `function One() @{
1405
- <div>"Hello"</div>
1409
+ <div>Hello</div>
1406
1410
  }`;
1407
1411
 
1408
1412
  const result = await format(input, {
@@ -1462,18 +1466,6 @@ export function Test({ a, b }: Props) {}`;
1462
1466
  expect(result).toBeWithNewline(expected);
1463
1467
  });
1464
1468
 
1465
- it('should not strip @ from dynamic @tag', async () => {
1466
- const expected = `export function Four() {
1467
- let tag = track('div');
1468
-
1469
- <@tag {href} {...props}>
1470
- <@children />
1471
- </@tag>
1472
- }`;
1473
- const result = await format(expected, { singleQuote: true, printWidth: 100 });
1474
- expect(result).toBeWithNewline(expected);
1475
- });
1476
-
1477
1469
  it('should not include a comma after the last rest parameter', async () => {
1478
1470
  const expected = `function Foo({
1479
1471
  lorem,
@@ -1490,15 +1482,6 @@ export function Test({ a, b }: Props) {}`;
1490
1482
  expect(result).toBeWithNewline(expected);
1491
1483
  });
1492
1484
 
1493
- it('should not strip @ from dynamic self-closing components', async () => {
1494
- const expected = `function App() {
1495
- <@ripple_object.tracked_basic />
1496
- }`;
1497
-
1498
- const result = await format(expected, { singleQuote: true, printWidth: 100 });
1499
- expect(result).toBeWithNewline(expected);
1500
- });
1501
-
1502
1485
  it('keeps a new line between comments above and code if one is present', async () => {
1503
1486
  const expected = `// comment
1504
1487
 
@@ -2545,6 +2528,51 @@ files = [...(files ?? []), ...dt.files];`;
2545
2528
  expect(result).toBeWithNewline(expected);
2546
2529
  });
2547
2530
 
2531
+ it('expands empty braces for template control-flow blocks', async () => {
2532
+ const input = `const App=()=> <>@if (ready) {} @else {}@for (const item of items) {} @empty {}</>;`;
2533
+ const expected = `const App = () => <>
2534
+ @if (ready) {
2535
+ } @else {
2536
+ }
2537
+ @for (const item of items) {
2538
+ } @empty {
2539
+ }
2540
+ </>;`;
2541
+ const result = await format(input);
2542
+ expect(result).toBeWithNewline(expected);
2543
+ });
2544
+
2545
+ it('expands empty braces for try family blocks', async () => {
2546
+ const input = `function Foo() @{ @try {} @pending {} @catch {} }
2547
+ function Bar() @{ @try {} @catch {} }
2548
+ function Baz() { try {} catch {} finally {} }
2549
+ function Qux() { try {} catch {} }`;
2550
+ const expected = `function Foo() @{
2551
+ @try {
2552
+ } @pending {
2553
+ } @catch {
2554
+ }
2555
+ }
2556
+ function Bar() @{
2557
+ @try {
2558
+ } @catch {
2559
+ }
2560
+ }
2561
+ function Baz() {
2562
+ try {
2563
+ } catch {
2564
+ } finally {
2565
+ }
2566
+ }
2567
+ function Qux() {
2568
+ try {
2569
+ } catch {
2570
+ }
2571
+ }`;
2572
+ const result = await format(input);
2573
+ expect(result).toBeWithNewline(expected);
2574
+ });
2575
+
2548
2576
  it('prints function with a rest parameter correctly', async () => {
2549
2577
  const expected = `function TestRest(...args: string[]) {
2550
2578
  console.log(args);
@@ -5604,7 +5632,7 @@ render(App);`;
5604
5632
  expect(result).toBeWithNewline(expected);
5605
5633
  });
5606
5634
 
5607
- it('should preserve the order of try / pending / catch block', async () => {
5635
+ it('should preserve the order of try / pending / catch blocks', async () => {
5608
5636
  const expected = `function Test() {
5609
5637
  let items: RippleArray<string> | null = null;
5610
5638
  let error: string | null = null;
@@ -5622,8 +5650,6 @@ render(App);`;
5622
5650
  <div>{'Loading...'}</div>
5623
5651
  } @catch (e) {
5624
5652
  error = (e as Error).message;
5625
- } finally {
5626
- <div>finally block</div>
5627
5653
  };
5628
5654
  }`;
5629
5655
 
@@ -5738,7 +5764,8 @@ render(App);`;
5738
5764
  // <div>
5739
5765
  @try {
5740
5766
  <div>b is true</div>
5741
- } @catch (e) {}
5767
+ } @catch (e) {
5768
+ }
5742
5769
  // <div>
5743
5770
  // <div>
5744
5771
  // @if (b) {
@@ -5756,7 +5783,8 @@ render(App);`;
5756
5783
  // <div>
5757
5784
  @try {
5758
5785
  <div>b is true</div>
5759
- } @catch (e) {}
5786
+ } @catch (e) {
5787
+ }
5760
5788
  // <div>
5761
5789
  // <div>
5762
5790
  // @if (b) {