@ripple-ts/prettier-plugin 0.2.153

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.
@@ -0,0 +1,3779 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import prettier from 'prettier';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ expect.extend({
10
+ toBeWithNewline(received, expected) {
11
+ const expectedWithNewline = expected.endsWith('\n') ? expected : expected + '\n';
12
+
13
+ const pass = received === expectedWithNewline;
14
+
15
+ return {
16
+ pass,
17
+ message: () => {
18
+ const { matcherHint, EXPECTED_COLOR, RECEIVED_COLOR } = this.utils;
19
+
20
+ // Just apply color without modifying the string
21
+ const formatWithColor = (str, colorFn) => {
22
+ return colorFn(str);
23
+ };
24
+
25
+ return (
26
+ matcherHint('toBeWithNewline') +
27
+ '\n\nExpected:\n' +
28
+ formatWithColor(expectedWithNewline, EXPECTED_COLOR) +
29
+ '\nReceived:\n' +
30
+ formatWithColor(received, RECEIVED_COLOR)
31
+ );
32
+ },
33
+ };
34
+ },
35
+ });
36
+
37
+ describe('prettier-plugin', () => {
38
+ /**
39
+ * @param {string} code
40
+ * @param {import('prettier').Options} [options]
41
+ */
42
+ const format = async (code, options = {}) => {
43
+ return await prettier.format(code, {
44
+ parser: 'ripple',
45
+ plugins: [join(__dirname, 'index.js')],
46
+ ...options,
47
+ });
48
+ };
49
+
50
+ /**
51
+ * @param {string} code
52
+ * @param {Partial<import('prettier').CursorOptions>} options
53
+ */
54
+ const formatWithCursorHelper = async (code, options = {}) => {
55
+ return await prettier.formatWithCursor(
56
+ code,
57
+ /** @type {import('prettier').CursorOptions} */ ({
58
+ parser: 'ripple',
59
+ plugins: [join(__dirname, 'index.js')],
60
+ ...options,
61
+ }),
62
+ );
63
+ };
64
+
65
+ describe('basic formatting', () => {
66
+ it('should format a simple component', async () => {
67
+ const input = `export component Test(){let count=0;<div>{"Hello"}</div>}`;
68
+ const expected = `export component Test() {
69
+ let count = 0;
70
+ <div>{'Hello'}</div>
71
+ }`;
72
+ const result = await format(input, { singleQuote: true });
73
+ expect(result).toBeWithNewline(expected);
74
+ });
75
+
76
+ it('should format a simple component with cursorOffset', async () => {
77
+ const input = `export component Test(){let count=0;<div>{"Hello"}</div>}`;
78
+ const expected = `export component Test() {
79
+ let count = 0;
80
+ <div>{'Hello'}</div>
81
+ }`;
82
+ const result = await formatWithCursorHelper(input, {
83
+ singleQuote: true,
84
+ cursorOffset: 50,
85
+ });
86
+ expect(result.formatted).toBeWithNewline(expected);
87
+ expect(typeof result.cursorOffset).toBe('number');
88
+ });
89
+
90
+ it('should format whitespace correctly', async () => {
91
+ const input = `export component Test(){
92
+ let count=0
93
+
94
+ // comment
95
+
96
+ <div>{"Hello"}</div>
97
+ <div>
98
+ let two=2
99
+
100
+ {"Hello"}
101
+ </div>
102
+ }`;
103
+ const expected = `export component Test() {
104
+ let count = 0;
105
+
106
+ // comment
107
+ <div>{'Hello'}</div>
108
+ <div>
109
+ let two = 2;
110
+
111
+ {'Hello'}
112
+ </div>
113
+ }`;
114
+ const result = await format(input, { singleQuote: true });
115
+ expect(result).toBeWithNewline(expected);
116
+ });
117
+
118
+ it('should format whitespace correctly #2', async () => {
119
+ const input = `export component Test(){
120
+ let count=0
121
+
122
+ const x = () => {
123
+ console.log("test");
124
+
125
+
126
+ if (x) {
127
+ console.log('test');
128
+ return null;
129
+ }
130
+
131
+ if (y) {
132
+
133
+ return null;
134
+
135
+ }
136
+
137
+
138
+ return x;
139
+ }
140
+
141
+ <div>{"Hello"}</div>
142
+ <div>
143
+ let two=2
144
+
145
+ {"Hello"}
146
+ </div>
147
+ }`;
148
+ const expected = `export component Test() {
149
+ let count = 0;
150
+
151
+ const x = () => {
152
+ console.log('test');
153
+
154
+ if (x) {
155
+ console.log('test');
156
+ return null;
157
+ }
158
+
159
+ if (y) {
160
+ return null;
161
+ }
162
+
163
+ return x;
164
+ };
165
+
166
+ <div>{'Hello'}</div>
167
+ <div>
168
+ let two = 2;
169
+
170
+ {'Hello'}
171
+ </div>
172
+ }`;
173
+ const result = await format(input, { singleQuote: true });
174
+ expect(result).toBeWithNewline(expected);
175
+ });
176
+
177
+ it('formatting already formatted code should not change it', async () => {
178
+ const already_formatted = `export component App() {
179
+ let $node;
180
+
181
+ const createRef = (node) => {
182
+ $node = node;
183
+ console.log('mounted', node);
184
+
185
+ return () => {
186
+ $node = undefined;
187
+ console.log('unmounted', node);
188
+ };
189
+ };
190
+
191
+ const arr = [1, 2, 3];
192
+ const obj = {
193
+ a: 1,
194
+ b: 2,
195
+ c: 3,
196
+ };
197
+
198
+ <div {ref createRef}>{'Hello world'}</div>
199
+
200
+ <style>
201
+ div {
202
+ color: blue;
203
+ }
204
+ </style>
205
+ }
206
+
207
+ function foo() {
208
+ // comment
209
+ }
210
+
211
+ export default component Basic() {
212
+ <div class="container">
213
+ <h1>{'Welcome to Ripple!'}</h1>
214
+ const items = [];
215
+
216
+ <div class="counter">
217
+ let $count = 0;
218
+
219
+ <button onClick={() => $count--}>{'-'}</button>
220
+ <span class="count">{$count}</span>
221
+ <button onClick={() => $count++}>{'+'}</button>
222
+ </div>
223
+ <div>
224
+ const foo = 'foo';
225
+
226
+ <p>{'This is a basic Ripple application template.'}</p>
227
+ <p>
228
+ {'Edit '}
229
+ <code>{'src/App.ripple'}</code>
230
+ {' to get started.'}
231
+ </p>
232
+ </div>
233
+ </div>
234
+ }`;
235
+ const formatted = await format(already_formatted, { singleQuote: true });
236
+
237
+ expect(formatted).toBeWithNewline(already_formatted);
238
+ });
239
+
240
+ it('formatting already formatted code should not change it #2', async () => {
241
+ const already_formatted = `import type { Component } from 'ripple';
242
+
243
+ export default component App() {
244
+ <div class="container">
245
+ let $count = 0;
246
+
247
+ <button onClick={() => $count++}>{$count}</button>
248
+
249
+ if ($count > 1) {
250
+ <div>{'Greater than 1!'}</div>
251
+ }
252
+ </div>
253
+
254
+ <style>
255
+ button {
256
+ padding: 1rem;
257
+ font-size: 1rem;
258
+ cursor: pointer;
259
+ }
260
+ </style>
261
+ }`;
262
+ const formatted = await format(already_formatted, { singleQuote: true });
263
+
264
+ expect(formatted).toBeWithNewline(already_formatted);
265
+ });
266
+
267
+ it('should format a component with an object property notation component markup', async () => {
268
+ const expected = `component Card(props) {
269
+ <div class="card">
270
+ <props.children />
271
+ </div>
272
+ }`;
273
+
274
+ const result = await format(expected, { singleQuote: true });
275
+ expect(result).toBeWithNewline(expected);
276
+ });
277
+
278
+ it('should format a component with an object reactive property notation props.@children', async () => {
279
+ const expected = `component Card(props) {
280
+ <div class="card">
281
+ <props.@children />
282
+ </div>
283
+ }`;
284
+
285
+ const result = await format(expected, { singleQuote: true });
286
+ expect(result).toBeWithNewline(expected);
287
+ });
288
+
289
+ it('should format a component with an object reactive bracketed property notation props.@["children"]', async () => {
290
+ const expected = `component Card(props) {
291
+ <div class="card">
292
+ <props.@['children'] />
293
+ </div>
294
+ }`;
295
+
296
+ const result = await format(expected, { singleQuote: true });
297
+ expect(result).toBeWithNewline(expected);
298
+ });
299
+
300
+ it('should respect print width when using ternary expressions', async () => {
301
+ const input = `function printMemberExpressionSimple(node, options, computed = false) {
302
+ if (node.type === 'MemberExpression') {
303
+ const prop = node.computed
304
+ ? (node.property.tracked ? '.@[' : '[') + printMemberExpressionSimple(node.property, options, node.computed) + ']'
305
+ : (node.property.tracked ? '.@' : '.') + printMemberExpressionSimple(node.property, options, node.computed);
306
+ }
307
+ }`;
308
+
309
+ const expected = `function printMemberExpressionSimple(
310
+ node,
311
+ options,
312
+ computed = false,
313
+ ) {
314
+ if (node.type === 'MemberExpression') {
315
+ const prop = node.computed
316
+ ? (node.property.tracked ? '.@[' : '[') +
317
+ printMemberExpressionSimple(
318
+ node.property,
319
+ options,
320
+ node.computed,
321
+ ) +
322
+ ']'
323
+ : (node.property.tracked ? '.@' : '.') +
324
+ printMemberExpressionSimple(
325
+ node.property,
326
+ options,
327
+ node.computed,
328
+ );
329
+ }
330
+ }`;
331
+
332
+ const result = await format(input, { singleQuote: true, printWidth: 70 });
333
+ expect(result).toBeWithNewline(expected);
334
+ });
335
+
336
+ it('should print nested ternary expressions with indentation', async () => {
337
+ const input = `const children_fn = b.arrow(
338
+ [b.id('__compat')],
339
+ needs_fragment
340
+ ? b.call(
341
+ '__compat._jsxs',
342
+ b.id('__compat.Fragment'),
343
+ b.object([
344
+ b.prop(
345
+ 'init',
346
+ b.id('children'),
347
+ b.array(normalized_children.map((child) => visit(child, state))),
348
+ ),
349
+ ]),
350
+ )
351
+ : visit(normalized_children[0], state),
352
+ );`;
353
+
354
+ const expected = `const children_fn = b.arrow(
355
+ [b.id('__compat')],
356
+ needs_fragment
357
+ ? b.call(
358
+ '__compat._jsxs',
359
+ b.id('__compat.Fragment'),
360
+ b.object([
361
+ b.prop(
362
+ 'init',
363
+ b.id('children'),
364
+ b.array(normalized_children.map((child) => visit(child, state))),
365
+ ),
366
+ ]),
367
+ )
368
+ : visit(normalized_children[0], state),
369
+ );`;
370
+
371
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
372
+ expect(result).toBeWithNewline(expected);
373
+ });
374
+
375
+ it('should properly format template literals with ternaries', async () => {
376
+ const input = `const handle_static_attr = (name, value) => {
377
+ const attr_str = \` \${name}\${is_boolean_attribute(name) && value === true
378
+ ? ''
379
+ : \`="\${value === true ? '' : escape_html(value, true)}"\`
380
+ }\`;
381
+
382
+ if (is_spreading) {
383
+ // For spread attributes, store just the actual value, not the full attribute string
384
+ const actual_value =
385
+ is_boolean_attribute(name) && value === true
386
+ ? b.literal(true)
387
+ : b.literal(value === true ? '' : value);
388
+ spread_attributes.push(b.prop('init', b.literal(name), actual_value));
389
+ } else {
390
+ state.init.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(attr_str))));
391
+ }
392
+ };`;
393
+
394
+ const expected = `const handle_static_attr = (name, value) => {
395
+ const attr_str = \` \${name}\${
396
+ is_boolean_attribute(name) && value === true
397
+ ? ''
398
+ : \`="\${value === true ? '' : escape_html(value, true)}"\`
399
+ }\`;
400
+
401
+ if (is_spreading) {
402
+ // For spread attributes, store just the actual value, not the full attribute string
403
+ const actual_value =
404
+ is_boolean_attribute(name) && value === true
405
+ ? b.literal(true)
406
+ : b.literal(value === true ? '' : value);
407
+ spread_attributes.push(b.prop('init', b.literal(name), actual_value));
408
+ } else {
409
+ state.init.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(attr_str))));
410
+ }
411
+ };`;
412
+
413
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
414
+ expect(result).toBeWithNewline(expected);
415
+ });
416
+
417
+ it('should format conditional expressions correctly', async () => {
418
+ const expected = `const consequentDoc =
419
+ hasUnparenthesizedNestedConditional &&
420
+ node.consequent.type === 'ConditionalExpression' &&
421
+ !node.consequent.metadata?.parenthesized
422
+ ? path.call(
423
+ (childPath) => print(childPath, { isNestedConditional: true }),
424
+ 'consequent',
425
+ )
426
+ : path.call(print, 'consequent');
427
+ const alternateDoc =
428
+ hasUnparenthesizedNestedConditional &&
429
+ node.alternate.type === 'ConditionalExpression' &&
430
+ !node.alternate.metadata?.parenthesized
431
+ ? path.call(
432
+ (childPath) => print(childPath, { isNestedConditional: true }),
433
+ 'alternate',
434
+ )
435
+ : path.call(print, 'alternate');`;
436
+
437
+ const result = await format(expected, { singleQuote: true, printWidth: 80 });
438
+ expect(result).toBeWithNewline(expected);
439
+ });
440
+
441
+ it('should format nested template literals correctly', async () => {
442
+ const expected = `const handle_static_attr = (name, value) => {
443
+ const attr_str = \` \${name}\${
444
+ is_boolean_attribute(name) && value === true
445
+ ? ''
446
+ : \`="\${value === true ? '' : escape_html(value, true)}"\`
447
+ }\`;
448
+ };`;
449
+
450
+ const result = await format(expected, { singleQuote: true, printWidth: 80 });
451
+ expect(result).toBeWithNewline(expected);
452
+ });
453
+
454
+ it('should respect print width when using conditional expressions with arrays', async () => {
455
+ const input = `const openingTag = group([
456
+ '<',
457
+ tagName,
458
+ hasAttributes
459
+ ? indent(
460
+ concat([
461
+ ...path.map((attrPath) => {
462
+ return concat([attrLineBreak, print(attrPath)]);
463
+ }, 'attributes'),
464
+ ]),
465
+ )
466
+ : '',
467
+ shouldUseSelfClosingSyntax
468
+ ? hasAttributes
469
+ ? line
470
+ : ''
471
+ : hasAttributes && !options.bracketSameLine
472
+ ? softline
473
+ : '',
474
+ shouldUseSelfClosingSyntax ? (hasAttributes ? '/>' : ' />') : '>',
475
+ ]);`;
476
+
477
+ const expected = `const openingTag = group([
478
+ '<',
479
+ tagName,
480
+ hasAttributes
481
+ ? indent(
482
+ concat([
483
+ ...path.map((attrPath) => {
484
+ return concat([attrLineBreak, print(attrPath)]);
485
+ }, 'attributes'),
486
+ ]),
487
+ )
488
+ : '',
489
+ shouldUseSelfClosingSyntax
490
+ ? hasAttributes
491
+ ? line
492
+ : ''
493
+ : hasAttributes && !options.bracketSameLine
494
+ ? softline
495
+ : '',
496
+ shouldUseSelfClosingSyntax ? (hasAttributes ? '/>' : ' />') : '>',
497
+ ]);`;
498
+
499
+ const result = await format(input, { singleQuote: true, printWidth: 70 });
500
+ expect(result).toBeWithNewline(expected);
501
+ });
502
+
503
+ it('should keep jsdoc on same line, spaces between, and parentheses', async () => {
504
+ const input = `/** @type {import('prettier').CursorOptions} */({});
505
+ const start = /** @type {any} */ (node).start;
506
+ /** @type {SomeType} */ (a) = 5;
507
+ function test() {
508
+ /** @type {SomeType} */ (a) = 5;
509
+ }
510
+ (node.trailingComments ||= []).push(
511
+ /** @type {CommentWithLocation} */(comments.shift()),
512
+ );
513
+ /** @type {number} */ (char.codePointAt(0)) >= 160`;
514
+ const expected = `/** @type {import('prettier').CursorOptions} */ ({});
515
+ const start = /** @type {any} */ (node).start;
516
+ /** @type {SomeType} */ (a) = 5;
517
+ function test() {
518
+ /** @type {SomeType} */ (a) = 5;
519
+ }
520
+ (node.trailingComments ||= []).push(
521
+ /** @type {CommentWithLocation} */ (comments.shift()),
522
+ );
523
+ /** @type {number} */ (char.codePointAt(0)) >= 160;`;
524
+
525
+ const result = await format(input, { singleQuote: true });
526
+ expect(result).toBeWithNewline(expected);
527
+ });
528
+
529
+ it('should not change formatting for function object properties and properties in square brackets', async () => {
530
+ const expected = `export component App() {
531
+ const SYMBOL_PROP = Symbol();
532
+
533
+ const obj = {
534
+ count: 0,
535
+ increment() {
536
+ this.count++;
537
+ },
538
+ [SYMBOL_PROP]() {
539
+ this.count++;
540
+ },
541
+ };
542
+ }`;
543
+
544
+ const result = await format(expected, { singleQuote: true });
545
+ expect(result).toBeWithNewline(expected);
546
+ });
547
+
548
+ it('should handle arrow functions with block bodies', async () => {
549
+ const input = `export component Test(){const handler=()=>{};handler}`;
550
+ const expected = `export component Test() {
551
+ const handler = () => {};
552
+ handler;
553
+ }`;
554
+ const result = await format(input, { singleQuote: true });
555
+ expect(result).toBeWithNewline(expected);
556
+ });
557
+
558
+ it('should handle style tags inside component body', async () => {
559
+ const input = `export component Test(){<div>{"Test"}</div><style>div{color:red}</style>}`;
560
+ const expected = `export component Test() {
561
+ <div>{'Test'}</div>
562
+ <style>
563
+ div {
564
+ color: red;
565
+ }
566
+ </style>
567
+ }`;
568
+ const result = await format(input, { singleQuote: true });
569
+ expect(result).toBeWithNewline(expected);
570
+ });
571
+
572
+ it('should handle TypeScript types and interfaces', async () => {
573
+ const input = `export component Test(){interface User{id:number;name:string}let user:User={id:1,name:"test"};user}`;
574
+ const expected = `export component Test() {
575
+ interface User {
576
+ id: number;
577
+ name: string;
578
+ }
579
+ let user: User = { id: 1, name: 'test' };
580
+ user;
581
+ }`;
582
+ const result = await format(input, { singleQuote: true });
583
+ expect(result).toBeWithNewline(expected);
584
+ });
585
+
586
+ it('should handle async/await in component body', async () => {
587
+ const input = `export component Test(){const data=await fetchData();data}`;
588
+ const expected = `export component Test() {
589
+ const data = await fetchData();
590
+ data;
591
+ }`;
592
+ const result = await format(input, { singleQuote: true });
593
+ expect(result).toBeWithNewline(expected);
594
+ });
595
+
596
+ it('should handle for...of loops in component body', async () => {
597
+ const input = `export component Test(){const items=[1,2,3];for(const item of items){<li>{item}</li>}}`;
598
+ const expected = `export component Test() {
599
+ const items = [1, 2, 3];
600
+ for (const item of items) {
601
+ <li>{item}</li>
602
+ }
603
+ }`;
604
+ const result = await format(input, { singleQuote: true });
605
+ expect(result).toBeWithNewline(expected);
606
+ });
607
+
608
+ it('should handle TypeScript function return type', async () => {
609
+ const input = `export component FooBar() { function Foo() : string { return ""; }}`;
610
+ const expected = `export component FooBar() {
611
+ function Foo(): string {
612
+ return '';
613
+ }
614
+ }`;
615
+ const result = await format(input, { singleQuote: true });
616
+ expect(result).toBeWithNewline(expected);
617
+ });
618
+
619
+ it('should handle TypeScript method return type', async () => {
620
+ const input = `class Foo { bar() : number { return 1; }}`;
621
+ const expected = `class Foo {
622
+ bar(): number {
623
+ return 1;
624
+ }
625
+ }`;
626
+ const result = await format(input, { singleQuote: true });
627
+ expect(result).toBeWithNewline(expected);
628
+ });
629
+
630
+ it('should handle import type statements', async () => {
631
+ const input = `import { type Component } from 'ripple';
632
+ import { Something, type Props, track } from 'ripple';`;
633
+ const expected = `import { type Component } from 'ripple';
634
+ import { Something, type Props, track } from 'ripple';`;
635
+ const result = await format(input, { singleQuote: true });
636
+ expect(result).toBeWithNewline(expected);
637
+ });
638
+
639
+ it('should handle @ prefix', async () => {
640
+ const input = `export default component App() {
641
+ <div>
642
+ let count = track(0);
643
+ @count = 2;
644
+ console.log(@count);
645
+ console.log(count);
646
+ if (@count > 1) {
647
+ <button onClick={() => @count++}>{@count}</button>
648
+ }
649
+ </div>
650
+ }`;
651
+ const expected = `export default component App() {
652
+ <div>
653
+ let count = track(0);
654
+ @count = 2;
655
+ console.log(@count);
656
+ console.log(count);
657
+ if (@count > 1) {
658
+ <button onClick={() => @count++}>{@count}</button>
659
+ }
660
+ </div>
661
+ }`;
662
+ const result = await format(input, { singleQuote: true });
663
+ expect(result).toBeWithNewline(expected);
664
+ });
665
+
666
+ it('should preserve @ symbol in JSX attributes and shorthand syntax', async () => {
667
+ const input = `component App() {
668
+ const count = track(0);
669
+
670
+ <Counter count={@count} />
671
+ <Counter {@count} />
672
+ }`;
673
+
674
+ const expected = `component App() {
675
+ const count = track(0);
676
+
677
+ <Counter {@count} />
678
+ <Counter {@count} />
679
+ }`;
680
+
681
+ const result = await format(input, { singleQuote: true });
682
+ expect(result).toBeWithNewline(expected);
683
+ });
684
+
685
+ it('should handle type annotations in object params', async () => {
686
+ const input = `interface Props {
687
+ a: number;
688
+ b: string;
689
+ }
690
+
691
+ export component Test({ a, b }: Props) {}`;
692
+
693
+ const expected = `interface Props {
694
+ a: number;
695
+ b: string;
696
+ }
697
+
698
+ export component Test({ a, b }: Props) {}`;
699
+ const result = await format(input, { singleQuote: true });
700
+ expect(result).toBeWithNewline(expected);
701
+ });
702
+
703
+ it('should handle inline type annotations in object params', async () => {
704
+ const input = `export component Test({ a, b}: { a: number; b: string }) {}`;
705
+ const expected = `export component Test({ a, b }: { a: number; b: string }) {}`;
706
+ const result = await format(input, { singleQuote: true });
707
+ expect(result).toBeWithNewline(expected);
708
+ });
709
+
710
+ it('respects the semi false option', async () => {
711
+ const input = `export component Test() {
712
+ const a = 1
713
+ const b = 2
714
+ <div>{a + b}</div>
715
+ }`;
716
+ const expected = `export component Test() {
717
+ const a = 1
718
+ const b = 2
719
+ <div>{a + b}</div>
720
+ }`;
721
+ const result = await format(input, { singleQuote: true, semi: false });
722
+ expect(result).toBeWithNewline(expected);
723
+ });
724
+
725
+ it('respects the semi true option', async () => {
726
+ const input = `export component Test() {
727
+ const a = 1
728
+ const b = 2
729
+ <div>{a + b}</div>
730
+ }`;
731
+ const expected = `export component Test() {
732
+ const a = 1;
733
+ const b = 2;
734
+ <div>{a + b}</div>
735
+ }`;
736
+ const result = await format(input, { singleQuote: true, semi: true });
737
+ expect(result).toBeWithNewline(expected);
738
+ });
739
+
740
+ it('should keep semi with tables in a for of loop', async () => {
741
+ const expected = `<table>
742
+ <tbody>
743
+ for (const row of items) {
744
+ const id = row.id;
745
+
746
+ <tr>
747
+ <td class="col-md-6" />
748
+ </tr>
749
+ }
750
+ </tbody>
751
+ </table>`;
752
+
753
+ const result = await format(expected, { singleQuote: true, semi: true });
754
+ expect(result).toBeWithNewline(expected);
755
+ });
756
+
757
+ it('should break up attributes on new lines if line length exceeds printWidth', async () => {
758
+ const expected = `component One() {
759
+ <button
760
+ class="some-class another-class yet-another-class class-with-a-long-name"
761
+ id="this-is-a-button"
762
+ >
763
+ {'this is a button'}
764
+ </button>
765
+ }`;
766
+
767
+ const result = await format(expected, { singleQuote: true, printWidth: 40 });
768
+ expect(result).toBeWithNewline(expected);
769
+ });
770
+
771
+ it('should handle bracketSameLine correctly', async () => {
772
+ const input = `component One() {
773
+ <button
774
+ class="some-class another-class yet-another-class class-with-a-long-name"
775
+ id="this-is-a-button"
776
+ >
777
+ {'this is a button'}
778
+ </button>
779
+ }`;
780
+
781
+ const expected = `component One() {
782
+ <button
783
+ class="some-class another-class yet-another-class class-with-a-long-name"
784
+ id="this-is-a-button">
785
+ {'this is a button'}
786
+ </button>
787
+ }`;
788
+
789
+ const result = await format(input, {
790
+ singleQuote: true,
791
+ printWidth: 40,
792
+ bracketSameLine: true,
793
+ });
794
+ expect(result).toBeWithNewline(expected);
795
+ });
796
+
797
+ it('should respect singleAttributePerLine set to true setting', async () => {
798
+ const input = `component One() {
799
+ <button
800
+ class="some-class" something="should" not="go" wrong="at all"
801
+ id="this-is-a-button"
802
+ >
803
+ {'this is a button'}
804
+ </button>
805
+ }`;
806
+
807
+ const expected = `component One() {
808
+ <button
809
+ class="some-class"
810
+ something="should"
811
+ not="go"
812
+ wrong="at all"
813
+ id="this-is-a-button"
814
+ >
815
+ {'this is a button'}
816
+ </button>
817
+ }`;
818
+
819
+ const result = await format(input, {
820
+ singleQuote: true,
821
+ printWidth: 100,
822
+ singleAttributePerLine: true,
823
+ });
824
+ expect(result).toBeWithNewline(expected);
825
+ });
826
+
827
+ it('should respect singleAttributePerLine set to false setting', async () => {
828
+ const input = `component One() {
829
+ <button
830
+ class="some-class"
831
+ something="should"
832
+ not="go"
833
+ wrong="at all"
834
+ id="this-is-a-button"
835
+ >
836
+ {'this is a button'}
837
+ </button>
838
+ }`;
839
+
840
+ const expected = `component One() {
841
+ <button class="some-class" something="should" not="go" wrong="at all" id="this-is-a-button">
842
+ {'this is a button'}
843
+ </button>
844
+ }`;
845
+
846
+ const result = await format(input, {
847
+ singleQuote: true,
848
+ printWidth: 100,
849
+ singleAttributePerLine: false,
850
+ });
851
+ expect(result).toBeWithNewline(expected);
852
+ });
853
+
854
+ it('should not format function parameter spread', async () => {
855
+ const expected = `component Two({ arg1, ...rest }) {}`;
856
+
857
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
858
+ expect(result).toBeWithNewline(expected);
859
+ });
860
+
861
+ it('should break up long function parameter spread on new lines if line length exceeds printWidth', async () => {
862
+ const input = `component Three({ argumentOne, argumentTwo, ArgumentThree, ArgumentFour, ArgumentFive, ArgumentSix, ArgumentSeven }) {}`;
863
+ const expected = `component Three({
864
+ argumentOne,
865
+ argumentTwo,
866
+ ArgumentThree,
867
+ ArgumentFour,
868
+ ArgumentFive,
869
+ ArgumentSix,
870
+ ArgumentSeven,
871
+ }) {}`;
872
+
873
+ const result = await format(input, { singleQuote: true, printWidth: 60 });
874
+ expect(result).toBeWithNewline(expected);
875
+ });
876
+
877
+ it('should not strip @ from dynamic @tag', async () => {
878
+ const expected = `export component Four() {
879
+ let tag = track('div');
880
+
881
+ <@tag {href} {...props}>
882
+ <@children />
883
+ </@tag>
884
+ }`;
885
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
886
+ expect(result).toBeWithNewline(expected);
887
+ });
888
+
889
+ it('should not include a comma after the last rest parameter', async () => {
890
+ const expected = `component Foo({
891
+ lorem,
892
+ ipsum,
893
+ dolor,
894
+ sit,
895
+ amet,
896
+ consectetur,
897
+ adipiscing,
898
+ ...rest
899
+ }) {}`;
900
+
901
+ const result = await format(expected, { singleQuote: true, printWidth: 60 });
902
+ expect(result).toBeWithNewline(expected);
903
+ });
904
+
905
+ it('keeps a new line between comments above and code if one is present', async () => {
906
+ const expected = `// comment
907
+
908
+ import { useCount, incrementCount } from './useCount';
909
+ import { effect, track } from 'ripple';`;
910
+
911
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
912
+ expect(result).toBeWithNewline(expected);
913
+ });
914
+
915
+ it('should format properly an array of objects', async () => {
916
+ const expected = `obj = {
917
+ test: [
918
+ { a: 1, b: 2, c: 3, d: 4 },
919
+ { a: 1, b: 2 },
920
+ { c: 3, d: 4 },
921
+ ],
922
+ };`;
923
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
924
+ expect(result).toBeWithNewline(expected);
925
+ });
926
+
927
+ it('should keep chained expression intact', async () => {
928
+ const expected = `const doc = getRootNode?.()?.ownerDocument ?? document;`;
929
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
930
+ expect(result).toBeWithNewline(expected);
931
+ });
932
+
933
+ it('does not add spaces around inlined array elements in destructured arguments', async () => {
934
+ const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {}
935
+ const [obj1, obj2] = arrayOfObjects;`;
936
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
937
+ expect(result).toBeWithNewline(expected);
938
+ });
939
+
940
+ it('properly formats for of loops where the parent has no attributes', async () => {
941
+ const expected = `<tbody>
942
+ for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
943
+ <tr class="not-last:border-b border-border/50">
944
+ <td class="py-2 font-mono w-48">
945
+ <Kbd>{key}</Kbd>
946
+ </td>
947
+ <td class="py-2">{value}</td>
948
+ </tr>
949
+ }
950
+ </tbody>`;
951
+
952
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
953
+ expect(result).toBeWithNewline(expected);
954
+ });
955
+
956
+ it('should keep a new line between elements or component if provided', async () => {
957
+ const expected = `<Something>
958
+ <div>{'Hello'}</div>
959
+ </Something>
960
+
961
+ <Child class="test" />`;
962
+
963
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
964
+ expect(result).toBeWithNewline(expected);
965
+ });
966
+
967
+ it('should keep proper formatting between css declarations', async () => {
968
+ const expected = `export component App() {
969
+ <style>
970
+ div {
971
+ background-color: red;
972
+ }
973
+ .even-class {
974
+ color: green;
975
+ }
976
+ .odd-class {
977
+ color: blue;
978
+ }
979
+ </style>
980
+ }`;
981
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
982
+ expect(result).toBeWithNewline(expected);
983
+ });
984
+
985
+ it('should keep one new line between css declarations if one or more is provided', async () => {
986
+ const input = `export component App() {
987
+ <style>
988
+ div {
989
+ background-color: red;
990
+ }
991
+
992
+ .even-class {
993
+ color: green;
994
+ }
995
+
996
+
997
+ .odd-class {
998
+ color: blue;
999
+ }
1000
+ </style>
1001
+ }`;
1002
+
1003
+ const expected = `export component App() {
1004
+ <style>
1005
+ div {
1006
+ background-color: red;
1007
+ }
1008
+
1009
+ .even-class {
1010
+ color: green;
1011
+ }
1012
+
1013
+ .odd-class {
1014
+ color: blue;
1015
+ }
1016
+ </style>
1017
+ }`;
1018
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
1019
+ expect(result).toBeWithNewline(expected);
1020
+ });
1021
+
1022
+ it('should keep style tag intact when wrapped in parent outside a component', async () => {
1023
+ const expected = `<head>
1024
+ <style>
1025
+ div {
1026
+ background: purple;
1027
+ }
1028
+ p {
1029
+ background: blue;
1030
+ }
1031
+ .div {
1032
+ color: red;
1033
+ }
1034
+ .p {
1035
+ color: green;
1036
+ }
1037
+ </style>
1038
+ </head>`;
1039
+
1040
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1041
+ expect(result).toBeWithNewline(expected);
1042
+ });
1043
+
1044
+ it('should keep style tag intact when wrapped in parent inside component', async () => {
1045
+ const expected = `component App() {
1046
+ <head>
1047
+ <style>
1048
+ div {
1049
+ background: purple;
1050
+ }
1051
+ p {
1052
+ background: blue;
1053
+ }
1054
+ .div {
1055
+ color: red;
1056
+ }
1057
+ .p {
1058
+ color: green;
1059
+ }
1060
+ </style>
1061
+ </head>
1062
+ }`;
1063
+
1064
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1065
+ expect(result).toBeWithNewline(expected);
1066
+ });
1067
+
1068
+ it('should keep css siblings formatting intact', async () => {
1069
+ const expected = `export component App() {
1070
+ <style>
1071
+ div + .div > div,
1072
+ p,
1073
+ #id + .div ~ div,
1074
+ #id {
1075
+ color: red;
1076
+ }
1077
+ </style>
1078
+ }`;
1079
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1080
+ expect(result).toBeWithNewline(expected);
1081
+ });
1082
+
1083
+ it('should format & parent nested selector correctly', async () => {
1084
+ const expected = `export component App() {
1085
+ <div>
1086
+ <h1>{'Hello'}</h1>
1087
+ </div>
1088
+ <style>
1089
+ div {
1090
+ & > * {
1091
+ color: blue;
1092
+ }
1093
+ }
1094
+ </style>
1095
+ }`;
1096
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1097
+ expect(result).toBeWithNewline(expected);
1098
+ });
1099
+
1100
+ it('should keep TrackedMap short syntax intact', async () => {
1101
+ const expected = `const map = new #Map([['key1', 'value1'], ['key2', 'value2']]);
1102
+ const set = new #Set([1, 2, 3]);`;
1103
+
1104
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1105
+ expect(result).toBeWithNewline(expected);
1106
+ });
1107
+
1108
+ it('should not remove blank lines between components and types if provided', async () => {
1109
+ const expected = `export component App() {
1110
+ console.log('test');
1111
+ }
1112
+
1113
+ type RootNode = ShadowRoot | Document | Node;
1114
+ type GetRootNode = () => RootNode;`;
1115
+
1116
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1117
+ expect(result).toBeWithNewline(expected);
1118
+ });
1119
+
1120
+ it('should preserve a blank line between components and js declarations if one is provided', async () => {
1121
+ const expected = `export component App() {
1122
+ <Card>
1123
+ component children() {
1124
+ <p class="highlighted">{'Card content here'}</p>
1125
+ }
1126
+ </Card>
1127
+
1128
+ const test = 5;
1129
+
1130
+ <div>{test}</div>
1131
+ }`;
1132
+
1133
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1134
+ expect(result).toBeWithNewline(expected);
1135
+ });
1136
+
1137
+ it('should preserve blank line between component with nested markup and js', async () => {
1138
+ const expected = `component App() {
1139
+ <div>
1140
+ const a = 1;
1141
+ <div>
1142
+ const b = 1;
1143
+ </div>
1144
+ <div>
1145
+ const b = 1;
1146
+ </div>
1147
+ </div>
1148
+ <div>
1149
+ const a = 2;
1150
+ <div>
1151
+ const b = 1;
1152
+ </div>
1153
+ </div>
1154
+ }
1155
+
1156
+ render(App);`;
1157
+
1158
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1159
+ expect(result).toBeWithNewline(expected);
1160
+ });
1161
+
1162
+ it('should not remove async from arrow functions', async () => {
1163
+ const expected = `describe('compat-react', async () => {
1164
+ const something = 10;
1165
+ });`;
1166
+
1167
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1168
+ expect(result).toBeWithNewline(expected);
1169
+ });
1170
+
1171
+ it('should preserve blank lines between components and various TS declarations', async () => {
1172
+ const expected = `export component App() {
1173
+ console.log('test');
1174
+ }
1175
+
1176
+ interface Props {
1177
+ value: string;
1178
+ }
1179
+
1180
+ type Result = string | number;
1181
+
1182
+ enum Status {
1183
+ Active,
1184
+ Inactive,
1185
+ Pending,
1186
+ }`;
1187
+
1188
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1189
+ expect(result).toBeWithNewline(expected);
1190
+ });
1191
+
1192
+ it('should preserve blank lines between ts and import statements', async () => {
1193
+ const expected = `export interface PortalActionProps {
1194
+ disabled?: boolean | undefined;
1195
+ container?: HTMLElement | undefined;
1196
+ getRootNode?: GetRootNode | undefined;
1197
+ }
1198
+
1199
+ import { Portal as RipplePortal } from 'ripple';`;
1200
+
1201
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1202
+ expect(result).toBeWithNewline(expected);
1203
+ });
1204
+
1205
+ it('should preserve blank lines between export statements and import statements or comments', async () => {
1206
+ const expected = `export { handler } from './test.ripple';
1207
+
1208
+ import { Portal as RipplePortal } from 'ripple';
1209
+
1210
+ // export { something } from './test.ripple;
1211
+
1212
+ import { GetRootNode } from './somewhere';`;
1213
+
1214
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1215
+ expect(result).toBeWithNewline(expected);
1216
+ });
1217
+
1218
+ it('should preserve export interface with extends as provided', async () => {
1219
+ const expected = `export interface TrackedArray<T> extends Array<T> {}`;
1220
+
1221
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1222
+ expect(result).toBeWithNewline(expected);
1223
+ });
1224
+
1225
+ it('should preserve ternaries and jsdoc type assertions with parens and space', async () => {
1226
+ const expected = `/**
1227
+ * @param {unknown} maybe_tracked
1228
+ * @param {'contentRect' | 'contentBoxSize' | 'borderBoxSize' | 'devicePixelContentBoxSize'} type
1229
+ */
1230
+ function bind_element_rect(maybe_tracked, type) {
1231
+ if (!is_tracked_object(maybe_tracked)) {
1232
+ throw not_tracked_type_error(\`bind\${type.charAt(0).toUpperCase() + type.slice(1)}()\`);
1233
+ }
1234
+
1235
+ var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
1236
+ var observer =
1237
+ type === 'contentRect' || type === 'contentBoxSize'
1238
+ ? resize_observer_content_box
1239
+ : type === 'borderBoxSize'
1240
+ ? resize_observer_border_box
1241
+ : resize_observer_device_pixel_content_box;
1242
+
1243
+ return (/** @type {HTMLElement} */ element) => {
1244
+ var unsubscribe = observer.observe(
1245
+ element,
1246
+ /** @param {any} entry */ (entry) => set(tracked, entry[type]),
1247
+ );
1248
+
1249
+ effect(() => unsubscribe);
1250
+ };
1251
+ }`;
1252
+
1253
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1254
+ expect(result).toBeWithNewline(expected);
1255
+ });
1256
+
1257
+ it('should preserve block comments formatting inside curly braces and inside markup', async () => {
1258
+ const expected = `<div class="container">{/* Dynamic SVG - the original problem case */}</div>`;
1259
+
1260
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1261
+ expect(result).toBeWithNewline(expected);
1262
+ });
1263
+
1264
+ it('should preserve block comments formatting inside curly braces and inside nested markup', async () => {
1265
+ const expected = `<div class="container">
1266
+ {/* Dynamic SVG - the original problem case */}
1267
+ <span>{'Content'}</span>
1268
+ {/* Static SVG - always worked */}
1269
+ <span>{'More Content'}</span>
1270
+ </div>`;
1271
+
1272
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1273
+ expect(result).toBeWithNewline(expected);
1274
+ });
1275
+
1276
+ it('should format function calls with long string literals correctly', async () => {
1277
+ const input = `for (const quasi of template.quasis) {
1278
+ quasi.value.raw = sanitize_template_string(/** @type {string} */(quasi.value.cooked));
1279
+ }`;
1280
+
1281
+ const expected = `for (const quasi of template.quasis) {
1282
+ quasi.value.raw = sanitize_template_string(
1283
+ /** @type {string} */ (quasi.value.cooked),
1284
+ );
1285
+ }`;
1286
+
1287
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1288
+ expect(result).toBeWithNewline(expected);
1289
+ });
1290
+
1291
+ it('should break up call expressions on new lines with inline jsdoc comments with printWidth 100', async () => {
1292
+ const input = `for (const quasi of template.quasis) {
1293
+ quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
1294
+ }
1295
+
1296
+ const program = /** @type {Program} */ (walk(/** @type {Node} */ (analysis.ast), { ...state, namespace: 'html' }, visitors));`;
1297
+
1298
+ const expected = `for (const quasi of template.quasis) {
1299
+ quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
1300
+ }
1301
+
1302
+ const program = /** @type {Program} */ (
1303
+ walk(/** @type {Node} */ (analysis.ast), { ...state, namespace: 'html' }, visitors)
1304
+ );`;
1305
+
1306
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
1307
+ expect(result).toBeWithNewline(expected);
1308
+ });
1309
+
1310
+ it('should break up call expressions on new lines with inline jsdoc comments with printWidth 30', async () => {
1311
+ const input = `for (const quasi of template.quasis) {
1312
+ quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
1313
+ }
1314
+
1315
+ const program = /** @type {Program} */ (walk(/** @type {Node} */ (analysis.ast), { ...state, namespace: 'html' }, visitors));`;
1316
+
1317
+ const expected = `for (const quasi of template.quasis) {
1318
+ quasi.value.raw =
1319
+ sanitize_template_string(
1320
+ /** @type {string} */ (
1321
+ quasi.value.cooked
1322
+ ),
1323
+ );
1324
+ }
1325
+
1326
+ const program =
1327
+ /** @type {Program} */ (
1328
+ walk(
1329
+ /** @type {Node} */ (
1330
+ analysis.ast
1331
+ ),
1332
+ {
1333
+ ...state,
1334
+ namespace: 'html',
1335
+ },
1336
+ visitors,
1337
+ )
1338
+ );`;
1339
+
1340
+ const result = await format(input, { singleQuote: true, printWidth: 30 });
1341
+ expect(result).toBeWithNewline(expected);
1342
+ });
1343
+
1344
+ it('should properly format long jsdoc with call expressions', async () => {
1345
+ const input = `const js = /** @type {ReturnType<typeof print> & { post_processing_changes?: PostProcessingChanges, line_offsets?: number[] }} */ (
1346
+ print(program, language_handler, {
1347
+ sourceMapContent: source,
1348
+ sourceMapSource: path.basename(filename),
1349
+ })
1350
+ );`;
1351
+
1352
+ const expected = `const js =
1353
+ /** @type {ReturnType<typeof print> & { post_processing_changes?: PostProcessingChanges, line_offsets?: number[] }} */ (
1354
+ print(program, language_handler, {
1355
+ sourceMapContent: source,
1356
+ sourceMapSource: path.basename(filename),
1357
+ })
1358
+ );`;
1359
+
1360
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
1361
+ expect(result).toBeWithNewline(expected);
1362
+ });
1363
+
1364
+ it('should expand call arguments containing a regex literal with a block callback', async () => {
1365
+ const input = String.raw`js.code = js.code.replace(/^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm, (match, p1, p2, offset) => {
1366
+ const replacement = p1 + p2;
1367
+ const line = offset_to_line(offset);
1368
+ const delta = replacement.length - match.length; // negative (removing 'declare ')
1369
+
1370
+ // Track first change offset and total delta per line
1371
+ if (!line_deltas.has(line)) {
1372
+ line_deltas.set(line, { offset, delta });
1373
+ } else {
1374
+ // Additional change on same line - accumulate delta
1375
+ // @ts-ignore
1376
+ line_deltas.get(line).delta += delta;
1377
+ }
1378
+ return replacement;
1379
+ });`;
1380
+
1381
+ const expected = String.raw`js.code = js.code.replace(
1382
+ /^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm,
1383
+ (match, p1, p2, offset) => {
1384
+ const replacement = p1 + p2;
1385
+ const line = offset_to_line(offset);
1386
+ const delta = replacement.length - match.length; // negative (removing 'declare ')
1387
+
1388
+ // Track first change offset and total delta per line
1389
+ if (!line_deltas.has(line)) {
1390
+ line_deltas.set(line, { offset, delta });
1391
+ } else {
1392
+ // Additional change on same line - accumulate delta
1393
+ // @ts-ignore
1394
+ line_deltas.get(line).delta += delta;
1395
+ }
1396
+ return replacement;
1397
+ },
1398
+ );`;
1399
+
1400
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1401
+ expect(result).toBeWithNewline(expected);
1402
+ });
1403
+
1404
+ it('should expand call arguments containing a regex literal with a block callback printWidth 40', async () => {
1405
+ const input = String.raw`js.code = js.code.replace(/^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm, (match, p1, p2, offset) => {
1406
+ const replacement = p1 + p2;
1407
+ const line = offset_to_line(offset);
1408
+ const delta = replacement.length - match.length; // negative (removing 'declare ')
1409
+
1410
+ // Track first change offset and total delta per line
1411
+ if (!line_deltas.has(line)) {
1412
+ line_deltas.set(line, { offset, delta });
1413
+ } else {
1414
+ // Additional change on same line - accumulate delta
1415
+ // @ts-ignore
1416
+ line_deltas.get(line).delta += delta;
1417
+ }
1418
+ return replacement;
1419
+ });`;
1420
+
1421
+ const expected = String.raw`js.code = js.code.replace(
1422
+ /^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm,
1423
+ (match, p1, p2, offset) => {
1424
+ const replacement = p1 + p2;
1425
+ const line = offset_to_line(offset);
1426
+ const delta =
1427
+ replacement.length - match.length; // negative (removing 'declare ')
1428
+
1429
+ // Track first change offset and total delta per line
1430
+ if (!line_deltas.has(line)) {
1431
+ line_deltas.set(line, {
1432
+ offset,
1433
+ delta,
1434
+ });
1435
+ } else {
1436
+ // Additional change on same line - accumulate delta
1437
+ // @ts-ignore
1438
+ line_deltas.get(line).delta +=
1439
+ delta;
1440
+ }
1441
+ return replacement;
1442
+ },
1443
+ );`;
1444
+
1445
+ const result = await format(input, { singleQuote: true, printWidth: 40 });
1446
+ expect(result).toBeWithNewline(expected);
1447
+ });
1448
+
1449
+ it('should expand call arguments containing a regex literal with a block callback printWidth 30', async () => {
1450
+ const input = String.raw`js.code = js.code.replace(/^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm, (match, p1, p2, offset) => {
1451
+ const replacement = p1 + p2;
1452
+ const line = offset_to_line(offset);
1453
+ const delta = replacement.length - match.length; // negative (removing 'declare ')
1454
+
1455
+ // Track first change offset and total delta per line
1456
+ if (!line_deltas.has(line)) {
1457
+ line_deltas.set(line, { offset, delta });
1458
+ } else {
1459
+ // Additional change on same line - accumulate delta
1460
+ // @ts-ignore
1461
+ line_deltas.get(line).delta += delta;
1462
+ }
1463
+ return replacement;
1464
+ });`;
1465
+
1466
+ const expected = String.raw`js.code = js.code.replace(
1467
+ /^(export\s+)declare\s+(function\s+\w+[^{\n]*;)$/gm,
1468
+ (match, p1, p2, offset) => {
1469
+ const replacement =
1470
+ p1 + p2;
1471
+ const line =
1472
+ offset_to_line(offset);
1473
+ const delta =
1474
+ replacement.length -
1475
+ match.length; // negative (removing 'declare ')
1476
+
1477
+ // Track first change offset and total delta per line
1478
+ if (
1479
+ !line_deltas.has(line)
1480
+ ) {
1481
+ line_deltas.set(line, {
1482
+ offset,
1483
+ delta,
1484
+ });
1485
+ } else {
1486
+ // Additional change on same line - accumulate delta
1487
+ // @ts-ignore
1488
+ line_deltas.get(
1489
+ line,
1490
+ ).delta += delta;
1491
+ }
1492
+ return replacement;
1493
+ },
1494
+ );`;
1495
+
1496
+ const result = await format(input, { singleQuote: true, printWidth: 30 });
1497
+ expect(result).toBeWithNewline(expected);
1498
+ });
1499
+ });
1500
+
1501
+ describe('edge cases', () => {
1502
+ it('should handle empty component', async () => {
1503
+ const input = 'export component Empty() {}';
1504
+ const result = await format(input);
1505
+ expect(result).toBeWithNewline('export component Empty() {}');
1506
+ });
1507
+
1508
+ it('should handle component with only style', async () => {
1509
+ const input = `export component Styled(){<style>body{background:#fff}</style>}`;
1510
+ const expected = `export component Styled() {
1511
+ <style>
1512
+ body {
1513
+ background: #fff;
1514
+ }
1515
+ </style>
1516
+ }`;
1517
+ const result = await format(input);
1518
+ expect(result).toBeWithNewline(expected);
1519
+ });
1520
+
1521
+ it('should handle empty component using cursor', async () => {
1522
+ const input = 'export component Empty() {}';
1523
+ const result = await format(input);
1524
+ expect(result).toBeWithNewline('export component Empty() {}');
1525
+ });
1526
+
1527
+ it('should handle component with only style', async () => {
1528
+ const input = `export component Styled(){<style>body{background:#fff}</style>}`;
1529
+ const expected = `export component Styled() {
1530
+ <style>
1531
+ body {
1532
+ background: #fff;
1533
+ }
1534
+ </style>
1535
+ }`;
1536
+ const result = await formatWithCursorHelper(input, { cursorOffset: 50 });
1537
+ expect(result.formatted).toBeWithNewline(expected);
1538
+ });
1539
+
1540
+ it('should correctly handle call expressions', async () => {
1541
+ const input = `export component App() {
1542
+ const context = track(globalContext.get().theme);
1543
+ <div>
1544
+ <TypedComponent />
1545
+ {@context}
1546
+ </div>
1547
+ }`;
1548
+
1549
+ const expected = `export component App() {
1550
+ const context = track(globalContext.get().theme);
1551
+ <div>
1552
+ <TypedComponent />
1553
+ {@context}
1554
+ </div>
1555
+ }`;
1556
+
1557
+ const result = await format(input);
1558
+ expect(result).toBeWithNewline(expected);
1559
+ });
1560
+
1561
+ it('should correctly handle TS syntax', async () => {
1562
+ const input = `type User = { name: string; age: number };
1563
+ let message: string[] = [];
1564
+
1565
+ // comments should be preserved
1566
+
1567
+ message.push(greet(\`Ripple\`));
1568
+ message.push(\`User: \${JSON.stringify({ name: 'Alice', age: 30 } as User)}\`);`;
1569
+
1570
+ const expected = `type User = { name: string; age: number };
1571
+ let message: string[] = [];
1572
+
1573
+ // comments should be preserved
1574
+
1575
+ message.push(greet(\`Ripple\`));
1576
+ message.push(\`User: \${JSON.stringify({ name: "Alice", age: 30 } as User)}\`);`;
1577
+
1578
+ const result = await format(input);
1579
+ expect(result).toBeWithNewline(expected);
1580
+ });
1581
+ });
1582
+
1583
+ it('should correctly handle inline jsx like comments', async () => {
1584
+ const input = `let message: string[] = []; // comments should be preserved
1585
+
1586
+ message.push(/* Some test comment */ greet(\`Ripple\`));
1587
+ `;
1588
+
1589
+ const expected = `let message: string[] = []; // comments should be preserved
1590
+
1591
+ message.push(/* Some test comment */ greet(\`Ripple\`));`;
1592
+
1593
+ const result = await format(input);
1594
+ expect(result).toBeWithNewline(expected);
1595
+ });
1596
+
1597
+ it('should correctly handle inline document like comments', async () => {
1598
+ const input = `let message: string[] = []; // comments should be preserved
1599
+
1600
+ message.push(/* Some test comment */ greet( /* Some text */ \`Ripple\`));
1601
+ `;
1602
+
1603
+ const expected = `let message: string[] = []; // comments should be preserved
1604
+
1605
+ message.push(/* Some test comment */ greet(/* Some text */ \`Ripple\`));`;
1606
+
1607
+ const result = await format(input);
1608
+ expect(result).toBeWithNewline(expected);
1609
+ });
1610
+
1611
+ it("should correctly handle comments according to Ripple's syntax", async () => {
1612
+ const input = `// input
1613
+ <section>
1614
+ // TODO
1615
+ {'Hello'}
1616
+ </section>
1617
+
1618
+ // input
1619
+ <section>
1620
+ // TODO
1621
+ </section>
1622
+
1623
+ // input
1624
+ <section>
1625
+ // TODO
1626
+ <span>{'Hello'}</span>
1627
+ </section>`;
1628
+
1629
+ const expected = `// input
1630
+ <section>
1631
+ // TODO
1632
+ {'Hello'}
1633
+ </section>
1634
+
1635
+ // input
1636
+ <section>
1637
+ // TODO
1638
+ </section>
1639
+
1640
+ // input
1641
+ <section>
1642
+ // TODO
1643
+ <span>{'Hello'}</span>
1644
+ </section>`;
1645
+
1646
+ const result = await format(input, { singleQuote: true });
1647
+ expect(result).toBeWithNewline(expected);
1648
+ });
1649
+
1650
+ it('should keep comments inside function with one statement at the top', async () => {
1651
+ const expected = `component App() {
1652
+ const something = 5;
1653
+ // comment
1654
+ }
1655
+
1656
+ function test() {
1657
+ const something = 5;
1658
+ // comment
1659
+ }`;
1660
+
1661
+ const result = await format(expected, { singleQuote: true });
1662
+ expect(result).toBeWithNewline(expected);
1663
+ });
1664
+
1665
+ it('should preserve trailing comments in function parameters', async () => {
1666
+ const expected = `function test(
1667
+ // comment in params
1668
+ a,
1669
+ // comment in params
1670
+ b,
1671
+ // comment in params
1672
+ c,
1673
+ // comment in params
1674
+ ) {}`;
1675
+
1676
+ const result = await format(expected, { singleQuote: true });
1677
+ expect(result).toBeWithNewline(expected);
1678
+ });
1679
+
1680
+ it('should preserve trailing comments in call arguments', async () => {
1681
+ const expected = `fn(
1682
+ arg1,
1683
+ // comment in args
1684
+ arg2,
1685
+ // comment in args
1686
+ arg3,
1687
+ // comment in args
1688
+ );`;
1689
+
1690
+ const result = await format(expected, { singleQuote: true });
1691
+ expect(result).toBeWithNewline(expected);
1692
+ });
1693
+
1694
+ it('should preserve trailing comments in arrow function parameters', async () => {
1695
+ const expected = `const test = (
1696
+ // comment in params
1697
+ a,
1698
+ // comment in params
1699
+ b,
1700
+ // comment in params
1701
+ c,
1702
+ // comment in params
1703
+ ) => {};`;
1704
+
1705
+ const result = await format(expected, { singleQuote: true });
1706
+ expect(result).toBeWithNewline(expected);
1707
+ });
1708
+
1709
+ it('should preserve trailing comments in class body', async () => {
1710
+ const expected = `class MyClass {
1711
+ /* comment 1 */
1712
+ method1() {}
1713
+ //comment 2
1714
+
1715
+ method2() {}
1716
+ // comment 3
1717
+ }`;
1718
+
1719
+ const result = await format(expected, { singleQuote: true });
1720
+ expect(result).toBeWithNewline(expected);
1721
+ });
1722
+
1723
+ it('should preserve comments in object and tracked object expressions', async () => {
1724
+ const expected = `const obj = {
1725
+ /* comment 1 */
1726
+ a: 1,
1727
+
1728
+ // comment 2
1729
+ b: 2,
1730
+ // comment 3
1731
+ };
1732
+
1733
+ const obj2 = #{
1734
+ /* comment 1 */
1735
+ a: 1,
1736
+
1737
+ // comment 2
1738
+ b: 2,
1739
+ // comment 3
1740
+ };`;
1741
+
1742
+ const result = await format(expected, { singleQuote: true });
1743
+ expect(result).toBeWithNewline(expected);
1744
+ });
1745
+
1746
+ it('should preserve comments in switch statement cases', async () => {
1747
+ const input = `switch (x) {
1748
+ case 1:
1749
+ foo();
1750
+ // comment 1
1751
+ case 2:
1752
+ bar();
1753
+ // comment 2
1754
+ }`;
1755
+
1756
+ const expected = `switch (x) {
1757
+ case 1:
1758
+ foo();
1759
+ // comment 1
1760
+ case 2:
1761
+ bar();
1762
+ // comment 2
1763
+ }`;
1764
+
1765
+ const result = await format(input, { singleQuote: true });
1766
+ expect(result).toBeWithNewline(expected);
1767
+ });
1768
+
1769
+ it('should preserve comments in arrays with width 80', async () => {
1770
+ const input = `const arr = [
1771
+ 1,
1772
+ /* comment 1 */
1773
+ 2,
1774
+ 3,
1775
+ // comment 2
1776
+ ];`;
1777
+
1778
+ const expected = `const arr = [
1779
+ 1, /* comment 1 */
1780
+ 2, 3,
1781
+ // comment 2
1782
+ ];`;
1783
+
1784
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1785
+ expect(result).toBeWithNewline(expected);
1786
+ });
1787
+
1788
+ it('should preserve comments in arrays width printWidth 3', async () => {
1789
+ const input = `const arr = #[
1790
+ 1,
1791
+ /* comment 1 */
1792
+ 2,
1793
+ 3,
1794
+ // comment 2
1795
+ ];`;
1796
+
1797
+ const expected = `const arr =
1798
+ #[
1799
+ 1,
1800
+ /* comment 1 */
1801
+ 2,
1802
+ 3,
1803
+ // comment 2
1804
+ ];`;
1805
+
1806
+ const result = await format(input, { singleQuote: true, printWidth: 3 });
1807
+ expect(result).toBeWithNewline(expected);
1808
+ });
1809
+
1810
+ it('should preserve comments in arrays width printWidth 13', async () => {
1811
+ const input = `const arr =
1812
+ #[
1813
+ 1 /* comment 1 */,
1814
+ 2, 3,
1815
+ // comment 2
1816
+ ];`;
1817
+
1818
+ const expected = `const arr = #[
1819
+ 1 /* comment 1 */,
1820
+ 2, 3,
1821
+ // comment 2
1822
+ ];`;
1823
+
1824
+ const result = await format(input, { singleQuote: true, printWidth: 13 });
1825
+ expect(result).toBeWithNewline(expected);
1826
+ });
1827
+
1828
+ it('should properly format array with various sized strings and 100 printWidth', async () => {
1829
+ const expected = `component App() {
1830
+ const d = [
1831
+ 'm14 12 4 4 4-4',
1832
+ 'M18 16V7',
1833
+ 'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
1834
+ 'M3.304 13h6.392',
1835
+ ];
1836
+ }`;
1837
+
1838
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
1839
+ expect(result).toBeWithNewline(expected);
1840
+ });
1841
+
1842
+ it('should correctly handle for loops with variable declarations', async () => {
1843
+ const input = `for (let i = 0, len = array.length; i < len; i++) {
1844
+ console.log(i);
1845
+ }`;
1846
+ const expected = `for (let i = 0, len = array.length; i < len; i++) {
1847
+ console.log(i);
1848
+ }`;
1849
+ const result = await format(input);
1850
+ expect(result).toBeWithNewline(expected);
1851
+ });
1852
+
1853
+ it('should correctly render attributes in template', async () => {
1854
+ const input = `export component App() {
1855
+ <div>
1856
+ <Expand name='' startingLength={20} />
1857
+ </div>
1858
+ }`;
1859
+
1860
+ const expected = `export component App() {
1861
+ <div>
1862
+ <Expand name="" startingLength={20} />
1863
+ </div>
1864
+ }`;
1865
+
1866
+ const result = await format(input);
1867
+ expect(result).toBeWithNewline(expected);
1868
+ });
1869
+
1870
+ it('should handle different attribute value types correctly', async () => {
1871
+ const input = `export component Test() {
1872
+ <div
1873
+ stringProp="hello"
1874
+ numberProp={42}
1875
+ booleanProp={true}
1876
+ falseProp={false}
1877
+ nullProp={null}
1878
+ expression={x + 1}
1879
+ />
1880
+ }`;
1881
+
1882
+ const expected = `export component Test() {
1883
+ <div stringProp="hello" numberProp={42} booleanProp={true} falseProp={false} nullProp={null} expression={x + 1} />
1884
+ }`;
1885
+
1886
+ const result = await format(input, { singleQuote: true, printWidth: 120 });
1887
+ expect(result).toBeWithNewline(expected);
1888
+ });
1889
+
1890
+ it('should handle default arguments correctly', async () => {
1891
+ const input = `component Expand({ name, startingLength = 10 }: { name: string; startingLength?: number }) {
1892
+ <div></div>
1893
+ }`;
1894
+
1895
+ const expected = `component Expand({
1896
+ name,
1897
+ startingLength = 10,
1898
+ }: {
1899
+ name: string;
1900
+ startingLength?: number;
1901
+ }) {
1902
+ <div />
1903
+ }`;
1904
+
1905
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1906
+ expect(result).toBeWithNewline(expected);
1907
+ });
1908
+
1909
+ it('should handle default arguments correctly in functions', async () => {
1910
+ const input = `function expand({ name, startingLength = 10 }: { name: string; startingLength?: number }) {
1911
+ return null;
1912
+ }`;
1913
+
1914
+ const expected = `function expand({
1915
+ name,
1916
+ startingLength = 10,
1917
+ }: {
1918
+ name: string;
1919
+ startingLength?: number;
1920
+ }) {
1921
+ return null;
1922
+ }`;
1923
+
1924
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1925
+ expect(result).toBeWithNewline(expected);
1926
+ });
1927
+
1928
+ it('should handle default arguments correctly in arrow functions', async () => {
1929
+ const input = `const expand = ({ name, startingLength = 10 }: { name: string; startingLength?: number }) => {
1930
+ return null;
1931
+ };`;
1932
+
1933
+ const expected = `const expand = ({
1934
+ name,
1935
+ startingLength = 10,
1936
+ }: {
1937
+ name: string;
1938
+ startingLength?: number;
1939
+ }) => {
1940
+ return null;
1941
+ };`;
1942
+
1943
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1944
+ expect(result).toBeWithNewline(expected);
1945
+ });
1946
+
1947
+ it('should handle array and object patterns correctly', async () => {
1948
+ const input = `for (const [i = 0, item] of items.entries()) {}
1949
+ for (const {i = 0, item} of items.entries()) {}`;
1950
+
1951
+ const expected = `for (const [i = 0, item] of items.entries()) {}
1952
+ for (const { i = 0, item } of items.entries()) {}`;
1953
+
1954
+ const result = await format(input);
1955
+ expect(result).toBeWithNewline(expected);
1956
+ });
1957
+
1958
+ it('should handle various other TS things', async () => {
1959
+ const input = `const globalContext = new Context<{ theme: string, array: number[] }>({ theme: 'light', array: [] });
1960
+ const items = [] as unknown[];`;
1961
+
1962
+ const expected = `const globalContext = new Context<{ theme: string; array: number[] }>({
1963
+ theme: 'light',
1964
+ array: [],
1965
+ });
1966
+ const items = [] as unknown[];`;
1967
+
1968
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
1969
+ expect(result).toBeWithNewline(expected);
1970
+ });
1971
+
1972
+ it('should correctly handle for loop with index syntax, plus comments', async () => {
1973
+ const input = `const test = () => {
1974
+ // some comments
1975
+ for (const item of []; index i) {
1976
+ // comment
1977
+ }
1978
+ debugger;
1979
+
1980
+ // some comments
1981
+ const test = ""; // some comments 2
1982
+ };`;
1983
+
1984
+ const result = await format(input);
1985
+ expect(result).toBeWithNewline(input);
1986
+ });
1987
+
1988
+ it('should format {html string} syntax correctly', async () => {
1989
+ const input = `export component App() {
1990
+ let source = \`
1991
+ <h1>My Blog Post</h1>
1992
+ <p>Hi! I like JS and Ripple.</p>
1993
+ \`
1994
+
1995
+ <article>
1996
+ {html source}
1997
+ </article>
1998
+ }`;
1999
+
2000
+ const expected = `export component App() {
2001
+ let source = \`
2002
+ <h1>My Blog Post</h1>
2003
+ <p>Hi! I like JS and Ripple.</p>
2004
+ \`;
2005
+
2006
+ <article>{html source}</article>
2007
+ }`;
2008
+
2009
+ const result = await format(input, { singleQuote: true });
2010
+ expect(result).toBeWithNewline(expected);
2011
+ });
2012
+
2013
+ it('should format {html expression} with different expressions', async () => {
2014
+ const input = `export component App(){
2015
+ <div>{html myHtml}</div>
2016
+ <div>{html "hello"}</div>
2017
+ <div>{html \`<b>test</b>\`}</div>
2018
+ }`;
2019
+
2020
+ const expected = `export component App() {
2021
+ <div>{html myHtml}</div>
2022
+ <div>{html 'hello'}</div>
2023
+ <div>{html \`<b>test</b>\`}</div>
2024
+ }`;
2025
+
2026
+ const result = await format(input, { singleQuote: true });
2027
+ expect(result).toBeWithNewline(expected);
2028
+ });
2029
+
2030
+ it('should not insert a new line between js and jsx if not provided', async () => {
2031
+ const expected = `export component App() {
2032
+ let text = 'something';
2033
+ <div>{String(text)}</div>
2034
+ }`;
2035
+
2036
+ const result = await format(expected, {
2037
+ singleQuote: true,
2038
+ arrowParens: 'always',
2039
+ printWidth: 100,
2040
+ });
2041
+ expect(result).toBeWithNewline(expected);
2042
+ });
2043
+
2044
+ it('should keep a new line between js and jsx if provided', async () => {
2045
+ const expected = `export component App() {
2046
+ let text = 'something';
2047
+ <div>{String(text)}</div>
2048
+ }`;
2049
+
2050
+ const result = await format(expected, {
2051
+ singleQuote: true,
2052
+ arrowParens: 'always',
2053
+ printWidth: 100,
2054
+ });
2055
+ expect(result).toBeWithNewline(expected);
2056
+ });
2057
+
2058
+ it('should not format html elements that fit on one line', async () => {
2059
+ const expected = `export component App() {
2060
+ <div class="container">
2061
+ <p>{'Some Random text'}</p>
2062
+ </div>
2063
+ }`;
2064
+
2065
+ const result = await format(expected, {
2066
+ singleQuote: true,
2067
+ arrowParens: 'always',
2068
+ printWidth: 100,
2069
+ });
2070
+
2071
+ expect(result).toBeWithNewline(expected);
2072
+ });
2073
+
2074
+ it('should format html elements that fit on one line', async () => {
2075
+ const input = `export component App() {
2076
+ <div class="container">
2077
+ <p>
2078
+ {'Some Random text'}
2079
+ </p>
2080
+ </div>
2081
+ }`;
2082
+
2083
+ const expected = `export component App() {
2084
+ <div class="container">
2085
+ <p>{'Some Random text'}</p>
2086
+ </div>
2087
+ }`;
2088
+
2089
+ const result = await format(input, {
2090
+ singleQuote: true,
2091
+ arrowParens: 'always',
2092
+ printWidth: 100,
2093
+ });
2094
+
2095
+ expect(result).toBeWithNewline(expected);
2096
+ });
2097
+
2098
+ it('should support jsxSingleQuote option', async () => {
2099
+ const input = `export component App() {
2100
+ <div class="container">
2101
+ <p>{'Some Random text'}</p>
2102
+ </div>
2103
+ }`;
2104
+
2105
+ const expected = `export component App() {
2106
+ <div class='container'>
2107
+ <p>{'Some Random text'}</p>
2108
+ </div>
2109
+ }`;
2110
+ const result = await format(input, { singleQuote: true, jsxSingleQuote: true });
2111
+
2112
+ expect(result).toBeWithNewline(expected);
2113
+ });
2114
+
2115
+ describe('TypeScript types', () => {
2116
+ it('should format all basic TypeScript primitive types', async () => {
2117
+ const input = `component TypeTest() {
2118
+ type t0 = undefined;
2119
+ type t1 = number;
2120
+ type t2 = string;
2121
+ type t3 = boolean;
2122
+ type t4 = null;
2123
+ type t5 = symbol;
2124
+ type t6 = bigint;
2125
+ type t7 = any;
2126
+ type t8 = unknown;
2127
+ type t9 = never;
2128
+ type t10 = void;
2129
+ <div>{"test"}</div>
2130
+ }`;
2131
+
2132
+ const expected = `component TypeTest() {
2133
+ type t0 = undefined;
2134
+ type t1 = number;
2135
+ type t2 = string;
2136
+ type t3 = boolean;
2137
+ type t4 = null;
2138
+ type t5 = symbol;
2139
+ type t6 = bigint;
2140
+ type t7 = any;
2141
+ type t8 = unknown;
2142
+ type t9 = never;
2143
+ type t10 = void;
2144
+ <div>{'test'}</div>
2145
+ }`;
2146
+
2147
+ const result = await format(input, { singleQuote: true });
2148
+ expect(result).toBeWithNewline(expected);
2149
+ });
2150
+
2151
+ it('should format TypeScript utility types', async () => {
2152
+ const input = `component UtilityTypeTest() {
2153
+ type t11 = { a: number; b: string };
2154
+ type t12 = keyof t11;
2155
+ const T0: t17 = { x: 1 };
2156
+ type t13 = typeof T0;
2157
+ type t14 = Partial<t11>;
2158
+ type t15 = Required<t14>;
2159
+ type t16 = Readonly<t15>;
2160
+ type t17 = Record<string, number>;
2161
+ type t18 = Pick<t11, 'a'>;
2162
+ type t19 = Omit<t11, 'b'>;
2163
+ type t20 = ReturnType<() => string>;
2164
+ type t21 = Parameters<(x: number, y: string) => void>;
2165
+ type t27 = new () => object;
2166
+ type t41 = ReturnType<typeof Math.max>;
2167
+ <div>{"test"}</div>
2168
+ }`;
2169
+
2170
+ const expected = `component UtilityTypeTest() {
2171
+ type t11 = { a: number; b: string };
2172
+ type t12 = keyof t11;
2173
+ const T0: t17 = { x: 1 };
2174
+ type t13 = typeof T0;
2175
+ type t14 = Partial<t11>;
2176
+ type t15 = Required<t14>;
2177
+ type t16 = Readonly<t15>;
2178
+ type t17 = Record<string, number>;
2179
+ type t18 = Pick<t11, 'a'>;
2180
+ type t19 = Omit<t11, 'b'>;
2181
+ type t20 = ReturnType<() => string>;
2182
+ type t21 = Parameters<(x: number, y: string) => void>;
2183
+ type t27 = new () => object;
2184
+ type t41 = ReturnType<typeof Math.max>;
2185
+ <div>{'test'}</div>
2186
+ }`;
2187
+
2188
+ const result = await format(input, { singleQuote: true });
2189
+ expect(result).toBeWithNewline(expected);
2190
+ });
2191
+
2192
+ it('should format TypeScript generics in variable declarations', async () => {
2193
+ const input = `component GenericTest() {
2194
+ let open: Tracked<boolean> = track(false);
2195
+ let items: Array<string> = [];
2196
+ let map: Map<string, number> = new Map();
2197
+ <div>{"test"}</div>
2198
+ }`;
2199
+
2200
+ const expected = `component GenericTest() {
2201
+ let open: Tracked<boolean> = track(false);
2202
+ let items: Array<string> = [];
2203
+ let map: Map<string, number> = new Map();
2204
+ <div>{'test'}</div>
2205
+ }`;
2206
+
2207
+ const result = await format(input, { singleQuote: true });
2208
+ expect(result).toBeWithNewline(expected);
2209
+ });
2210
+
2211
+ it('should format TypeScript union and intersection types', async () => {
2212
+ const input = `component UnionTest() {
2213
+ type StringOrNumber = string | number;
2214
+ type Props = { a: string } & { b: number };
2215
+ let value: string | null = null;
2216
+ <div>{"test"}</div>
2217
+ }`;
2218
+
2219
+ const expected = `component UnionTest() {
2220
+ type StringOrNumber = string | number;
2221
+ type Props = { a: string } & { b: number };
2222
+ let value: string | null = null;
2223
+ <div>{'test'}</div>
2224
+ }`;
2225
+
2226
+ const result = await format(input, { singleQuote: true });
2227
+ expect(result).toBeWithNewline(expected);
2228
+ });
2229
+
2230
+ it('should format TypeScript tuple types (TSTupleType)', async () => {
2231
+ const input = `type T = [string, number, boolean];`;
2232
+ const expected = `type T = [string, number, boolean];`;
2233
+ const result = await format(input);
2234
+ expect(result).toBeWithNewline(expected);
2235
+ });
2236
+
2237
+ it('should format TypeScript index signatures (TSIndexSignature)', async () => {
2238
+ const input = `interface Dict { [key: string]: number; readonly [id: number]: string }`;
2239
+ const expected = `interface Dict {\n [key: string]: number;\n readonly [id: number]: string;\n}`;
2240
+ const result = await format(input);
2241
+ expect(result).toBeWithNewline(expected);
2242
+ });
2243
+
2244
+ it('should format TypeScript constructor types (TSConstructorType)', async () => {
2245
+ const input = `type Ctor = new (x: number, y: string) => Foo;`;
2246
+ const expected = `type Ctor = new (x: number, y: string) => Foo;`;
2247
+ const result = await format(input);
2248
+ expect(result).toBeWithNewline(expected);
2249
+ });
2250
+
2251
+ it('should format TypeScript conditional types (TSConditionalType)', async () => {
2252
+ const input = `type T = string extends string ? number : boolean;`;
2253
+ const expected = `type T = string extends string ? number : boolean;`;
2254
+ const result = await format(input);
2255
+ expect(result).toBeWithNewline(expected);
2256
+ });
2257
+
2258
+ it('should format TypeScript mapped types (TSMappedType)', async () => {
2259
+ const input = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] }`;
2260
+ const expected = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] };`;
2261
+ const result = await format(input);
2262
+ expect(result).toBeWithNewline(expected);
2263
+ });
2264
+
2265
+ it('should format TypeScript qualified names (TSQualifiedName)', async () => {
2266
+ const input = `type T = Foo.Bar;`;
2267
+ const expected = `type T = Foo.Bar;`;
2268
+ const result = await format(input);
2269
+ expect(result).toBeWithNewline(expected);
2270
+ });
2271
+
2272
+ it('should format TypeScript indexed access types (TSIndexedAccessType)', async () => {
2273
+ const input = `type V = Props["value"]; type W = Map<string, number>["size"]; type X = T[K];`;
2274
+ const expected = `type V = Props["value"];\ntype W = Map<string, number>["size"];\ntype X = T[K];`;
2275
+ const result = await format(input);
2276
+ expect(result).toBeWithNewline(expected);
2277
+ });
2278
+
2279
+ it('should properly format TSParenthesizedType', async () => {
2280
+ const expected = `const logs: (number | undefined)[] = [];`;
2281
+ const result = await format(expected);
2282
+ expect(result).toBeWithNewline(expected);
2283
+ });
2284
+
2285
+ it('should retain templated declarations', async () => {
2286
+ const expected = `function Wrapper() {
2287
+ return {
2288
+ unwrap: function <T>() {
2289
+ return null as unknown as T;
2290
+ },
2291
+ };
2292
+ }
2293
+
2294
+ class Box<T> {
2295
+ value: T;
2296
+
2297
+ method<T>(): T {
2298
+ return this.value;
2299
+ }
2300
+ }
2301
+
2302
+ function Wrapper2<T>(arg: T) {
2303
+ let x: T = arg;
2304
+ return {
2305
+ unwrap: function <T>() {
2306
+ return null as unknown as T;
2307
+ },
2308
+ do: function (): T {
2309
+ return x;
2310
+ },
2311
+ };
2312
+ }
2313
+
2314
+ const fn = <T>(arg: T): T => arg;`;
2315
+
2316
+ const result = await format(expected, { singleQuote: true });
2317
+ expect(result).toBeWithNewline(expected);
2318
+ });
2319
+
2320
+ it('respects arrowParens option', async () => {
2321
+ const input = `function inputRef(node) {
2322
+ const removeListener = on(node, 'input', e => { value = e.target.value; console.log(value) });
2323
+
2324
+ return () => { removeListener(); }
2325
+ }`;
2326
+
2327
+ const expected = `function inputRef(node) {
2328
+ const removeListener = on(node, 'input', (e) => {
2329
+ value = e.target.value;
2330
+ console.log(value);
2331
+ });
2332
+
2333
+ return () => {
2334
+ removeListener();
2335
+ };
2336
+ }`;
2337
+
2338
+ const result = await format(input, { singleQuote: true, arrowParens: 'always' });
2339
+ expect(result).toBeWithNewline(expected);
2340
+ });
2341
+
2342
+ it('keeps one new line between comment blocks and code if 1 or more exist', async () => {
2343
+ const input = `// comments
2344
+ //comments
2345
+
2346
+
2347
+ //comments
2348
+ function inputRef(node) {
2349
+ console.log('ref called');
2350
+ const removeListener = on(node, 'input', (e) => { value = e.target.value; console.log(value) });
2351
+ return () => {
2352
+ removeListener();
2353
+ }
2354
+ }
2355
+
2356
+ // some comment
2357
+ // more comments here
2358
+
2359
+ //now more comments
2360
+ // and some more
2361
+
2362
+
2363
+
2364
+
2365
+
2366
+
2367
+
2368
+
2369
+ //yet more`;
2370
+
2371
+ const expected = `// comments
2372
+ //comments
2373
+
2374
+ //comments
2375
+ function inputRef(node) {
2376
+ console.log('ref called');
2377
+ const removeListener = on(node, 'input', (e) => {
2378
+ value = e.target.value;
2379
+ console.log(value);
2380
+ });
2381
+ return () => {
2382
+ removeListener();
2383
+ };
2384
+ }
2385
+
2386
+ // some comment
2387
+ // more comments here
2388
+
2389
+ //now more comments
2390
+ // and some more
2391
+
2392
+ //yet more`;
2393
+
2394
+ const result = await format(input, { singleQuote: true, arrowParens: 'always' });
2395
+ expect(result).toBeWithNewline(expected);
2396
+ });
2397
+
2398
+ it('keeps one new line comments and functions when 1 or more exist', async () => {
2399
+ const input = `export component App() {
2400
+ // try {
2401
+ doSomething()
2402
+ // } catch {
2403
+ // somethingElse()
2404
+ // }
2405
+
2406
+
2407
+
2408
+ try {
2409
+ doSomething();
2410
+ } catch {
2411
+ somethingElse();
2412
+ }
2413
+ }`;
2414
+
2415
+ const expected = `export component App() {
2416
+ // try {
2417
+ doSomething();
2418
+ // } catch {
2419
+ // somethingElse()
2420
+ // }
2421
+
2422
+ try {
2423
+ doSomething();
2424
+ } catch {
2425
+ somethingElse();
2426
+ }
2427
+ }`;
2428
+
2429
+ const result = await format(input, { singleQuote: true, arrowParens: 'always' });
2430
+ expect(result).toBeWithNewline(expected);
2431
+ });
2432
+
2433
+ it('correctly formats array of objects and keys as either literals or identifiers', async () => {
2434
+ const input = `const tt = [
2435
+ {
2436
+ "id": "toast:2",
2437
+ "stacked": false,
2438
+ },
2439
+ {
2440
+ "id": "toast:3",
2441
+ "stacked": false,
2442
+ },
2443
+ {
2444
+ "id": "toast:4",
2445
+ "stacked": false,
2446
+ },
2447
+ {
2448
+ "id-literal": "toast:5",
2449
+ "stacked": false,
2450
+ },
2451
+ {
2452
+ "id": "toast:6",
2453
+ "stacked": false,
2454
+ }
2455
+ ];`;
2456
+
2457
+ const expected = `const tt = [
2458
+ {
2459
+ id: 'toast:2',
2460
+ stacked: false,
2461
+ },
2462
+ {
2463
+ id: 'toast:3',
2464
+ stacked: false,
2465
+ },
2466
+ {
2467
+ id: 'toast:4',
2468
+ stacked: false,
2469
+ },
2470
+ {
2471
+ 'id-literal': 'toast:5',
2472
+ stacked: false,
2473
+ },
2474
+ {
2475
+ id: 'toast:6',
2476
+ stacked: false,
2477
+ },
2478
+ ];`;
2479
+
2480
+ const result = await format(input, { singleQuote: true, arrowParens: 'always' });
2481
+ expect(result).toBeWithNewline(expected);
2482
+ });
2483
+
2484
+ it('properly formats components markup and new lines and leaves one new line between components and <style> if one or more exits', async () => {
2485
+ const input = `export component App() {
2486
+ <div>
2487
+ <RowList rows={#[{id: 'a'}, {id: 'b'}, {id: 'c'}]}>
2488
+ component Row({id, index, isHighlighted = (index) => (index % 2) === 0}) {
2489
+ <div class={{highlighted: isHighlighted(index)}}>{index}{' - '}{id}</div>
2490
+
2491
+ <style>
2492
+ .highlighted {
2493
+ background-color: lightgray;
2494
+ color: black;
2495
+ }
2496
+ </style>
2497
+ }
2498
+ </RowList>
2499
+ </div>
2500
+ }
2501
+
2502
+ component RowList({ rows, Row }) {
2503
+ for (const { id } of rows; index i;) {
2504
+ <Row index={i} {id} />
2505
+ }
2506
+ }`;
2507
+
2508
+ const expected = `export component App() {
2509
+ <div>
2510
+ <RowList rows={#[{id: 'a'}, {id: 'b'}, {id: 'c'}]}>
2511
+ component Row({ id, index, isHighlighted = (index) => index % 2 === 0 }) {
2512
+ <div class={{highlighted: isHighlighted(index)}}>
2513
+ {index}
2514
+ {' - '}
2515
+ {id}
2516
+ </div>
2517
+
2518
+ <style>
2519
+ .highlighted {
2520
+ background-color: lightgray;
2521
+ color: black;
2522
+ }
2523
+ </style>
2524
+ }
2525
+ </RowList>
2526
+ </div>
2527
+ }
2528
+
2529
+ component RowList({ rows, Row }) {
2530
+ for (const { id } of rows; index i) {
2531
+ <Row index={i} {id} />
2532
+ }
2533
+ }`;
2534
+ const result = await format(input, {
2535
+ singleQuote: true,
2536
+ arrowParens: 'always',
2537
+ printWidth: 100,
2538
+ });
2539
+ expect(result).toBeWithNewline(expected);
2540
+ });
2541
+
2542
+ it('leaves the shorthand reactive declaration intact and formats the same way as plain objects', async () => {
2543
+ const input = `export component App() {
2544
+ const obj = #{ a: 1, b: 2, c: 3 };
2545
+ let singleUser = #{name:"Test Me", email: "abc@example.com"}
2546
+ }`;
2547
+
2548
+ const expected = `export component App() {
2549
+ const obj = #{ a: 1, b: 2, c: 3 };
2550
+ let singleUser = #{ name: 'Test Me', email: 'abc@example.com' };
2551
+ }`;
2552
+ const result = await format(input, {
2553
+ singleQuote: true,
2554
+ arrowParens: 'always',
2555
+ printWidth: 100,
2556
+ });
2557
+ expect(result).toBeWithNewline(expected);
2558
+ });
2559
+
2560
+ it('formats single line reactive object into multiline when printWidth is exceeded', async () => {
2561
+ const input = `export component App() {
2562
+ const obj = #{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15};
2563
+ let singleUser = #{name:"Test Me", email: "abc@example.com"}
2564
+ }`;
2565
+
2566
+ const expected = `export component App() {
2567
+ const obj = #{
2568
+ a: 1,
2569
+ b: 2,
2570
+ c: 3,
2571
+ d: 4,
2572
+ e: 5,
2573
+ f: 6,
2574
+ g: 7,
2575
+ h: 8,
2576
+ i: 9,
2577
+ j: 10,
2578
+ k: 11,
2579
+ l: 12,
2580
+ m: 13,
2581
+ n: 14,
2582
+ o: 15,
2583
+ };
2584
+ let singleUser = #{ name: 'Test Me', email: 'abc@example.com' };
2585
+ }`;
2586
+ const result = await format(input, {
2587
+ singleQuote: true,
2588
+ arrowParens: 'always',
2589
+ printWidth: 100,
2590
+ });
2591
+ expect(result).toBeWithNewline(expected);
2592
+ });
2593
+
2594
+ it('leaves the shorthand reactive array declaration intact and formats the same way as regular array', async () => {
2595
+ const input = `export component App() {
2596
+ const arr = #[ {a: 1}, { b:2}, {c:3 } ];
2597
+ let multi = #[{a: 1}, {b: 2}, {c: 3}, {d: 4}, {e:5}, {f:6}, {g: 7}, {h: 8}, {i:9}, {j: 10}, {k: 11}];
2598
+ }`;
2599
+
2600
+ const expected = `export component App() {
2601
+ const arr = #[{ a: 1 }, { b: 2 }, { c: 3 }];
2602
+ let multi = #[
2603
+ { a: 1 },
2604
+ { b: 2 },
2605
+ { c: 3 },
2606
+ { d: 4 },
2607
+ { e: 5 },
2608
+ { f: 6 },
2609
+ { g: 7 },
2610
+ { h: 8 },
2611
+ { i: 9 },
2612
+ { j: 10 },
2613
+ { k: 11 },
2614
+ ];
2615
+ }`;
2616
+ const result = await format(input, {
2617
+ singleQuote: true,
2618
+ arrowParens: 'always',
2619
+ printWidth: 100,
2620
+ });
2621
+ expect(result).toBeWithNewline(expected);
2622
+ });
2623
+
2624
+ it('preserves typescript parameter types with a default value', async () => {
2625
+ const expected = `function getString(e: string = 'test') {
2626
+ return e;
2627
+ }`;
2628
+ const result = await format(expected, { singleQuote: true });
2629
+ expect(result).toBeWithNewline(expected);
2630
+ });
2631
+
2632
+ it('should format TypeScript enums', async () => {
2633
+ const input = `enum Color{Red,Green,Blue}`;
2634
+ const expected = `enum Color {
2635
+ Red,
2636
+ Green,
2637
+ Blue,
2638
+ }`;
2639
+ const result = await format(input, { singleQuote: true });
2640
+ expect(result).toBeWithNewline(expected);
2641
+ });
2642
+
2643
+ it('should format TypeScript enums with values', async () => {
2644
+ const input = `enum Status{Active=1,Inactive=0,Pending=2}`;
2645
+ const expected = `enum Status {
2646
+ Active = 1,
2647
+ Inactive = 0,
2648
+ Pending = 2,
2649
+ }`;
2650
+ const result = await format(input, { singleQuote: true });
2651
+ expect(result).toBeWithNewline(expected);
2652
+ });
2653
+
2654
+ it('should format const enums', async () => {
2655
+ const input = `const enum Direction{Up,Down,Left,Right}`;
2656
+ const expected = `const enum Direction {
2657
+ Up,
2658
+ Down,
2659
+ Left,
2660
+ Right,
2661
+ }`;
2662
+ const result = await format(input, { singleQuote: true });
2663
+ expect(result).toBeWithNewline(expected);
2664
+ });
2665
+
2666
+ it('should respect trailingComma option for enums', async () => {
2667
+ const input = `enum Size{Small,Medium,Large}`;
2668
+ const expected = `enum Size {
2669
+ Small,
2670
+ Medium,
2671
+ Large
2672
+ }`;
2673
+ const result = await format(input, { singleQuote: true, trailingComma: 'none' });
2674
+ expect(result).toBeWithNewline(expected);
2675
+ });
2676
+
2677
+ it('should format enums with string values', async () => {
2678
+ const input = `enum Colors{Red='red',Green='green',Blue='blue'}`;
2679
+ const expected = `enum Colors {
2680
+ Red = 'red',
2681
+ Green = 'green',
2682
+ Blue = 'blue',
2683
+ }`;
2684
+ const result = await format(input, { singleQuote: true });
2685
+ expect(result).toBeWithNewline(expected);
2686
+ });
2687
+
2688
+ it('should keep the return type annotation intact on an arrow function', async () => {
2689
+ const expected = `const getParams = (): Params<T> => ({});
2690
+ interface Params<T> {}`;
2691
+
2692
+ const result = await format(expected, { singleQuote: true });
2693
+ expect(result).toBeWithNewline(expected);
2694
+ });
2695
+ });
2696
+
2697
+ describe('regex formatting', () => {
2698
+ it('preserves regex literals in method calls', async () => {
2699
+ const expected = `export component App() {
2700
+ let text = 'Hello <span>world</span>';
2701
+ let result = text.match(/<span>/);
2702
+ <div>{String(result)}</div>
2703
+ }`;
2704
+
2705
+ const result = await format(expected, {
2706
+ singleQuote: true,
2707
+ arrowParens: 'always',
2708
+ printWidth: 100,
2709
+ });
2710
+
2711
+ expect(result).toBeWithNewline(expected);
2712
+ });
2713
+
2714
+ it('preserves multiple regex patterns', async () => {
2715
+ const expected = `export component App() {
2716
+ let html = '<div>Hello</div><span>World</span>';
2717
+ let divMatch = html.match(/<div>/g);
2718
+ let spanReplace = html.replace(/<span>/g, '[SPAN]');
2719
+ let allTags = html.split(/<br>/);
2720
+ }`;
2721
+
2722
+ const result = await format(expected, {
2723
+ singleQuote: true,
2724
+ arrowParens: 'always',
2725
+ printWidth: 100,
2726
+ });
2727
+
2728
+ expect(result).toBeWithNewline(expected);
2729
+ });
2730
+
2731
+ it('preserves regex literals in variable assignments', async () => {
2732
+ const expected = `export component App() {
2733
+ let spanRegex = /<span>/g;
2734
+ let divRegex = /<div>/;
2735
+ let simpleRegex = /<br>/g;
2736
+ }`;
2737
+
2738
+ const result = await format(expected, {
2739
+ singleQuote: true,
2740
+ arrowParens: 'always',
2741
+ printWidth: 100,
2742
+ });
2743
+
2744
+ expect(result).toBeWithNewline(expected);
2745
+ });
2746
+
2747
+ it('distinguishes regex from JSX', async () => {
2748
+ const expected = `export component App() {
2749
+ let htmlString = '<p>Paragraph</p>';
2750
+ let paragraphs = htmlString.match(/<p>/g);
2751
+ <div class="container">
2752
+ <p>{'Some Random text'}</p>
2753
+ </div>
2754
+ }`;
2755
+
2756
+ const result = await format(expected, {
2757
+ singleQuote: true,
2758
+ arrowParens: 'always',
2759
+ printWidth: 100,
2760
+ });
2761
+
2762
+ expect(result).toBeWithNewline(expected);
2763
+ });
2764
+
2765
+ it('should handle edge case regex patterns', async () => {
2766
+ const expected = `export component Test() {
2767
+ let text = '<<test>> <span>content</span>';
2768
+ let multiAngle = text.match(/<span>/);
2769
+ let simplePattern = text.match(/<>/);
2770
+ }`;
2771
+
2772
+ const result = await format(expected, {
2773
+ singleQuote: true,
2774
+ arrowParens: 'always',
2775
+ printWidth: 100,
2776
+ });
2777
+
2778
+ expect(result).toBeWithNewline(expected);
2779
+ });
2780
+ });
2781
+
2782
+ describe('blank line rules', () => {
2783
+ describe('Rule A: Collapse multiple blank lines to one', () => {
2784
+ it('collapses multiple blank lines between statements', async () => {
2785
+ const input = `export component App() {
2786
+ let a = 1;
2787
+
2788
+
2789
+ let b = 2;
2790
+ }`;
2791
+
2792
+ const expected = `export component App() {
2793
+ let a = 1;
2794
+
2795
+ let b = 2;
2796
+ }`;
2797
+
2798
+ const result = await format(input, { singleQuote: true });
2799
+ expect(result).toBeWithNewline(expected);
2800
+ });
2801
+
2802
+ it('collapses multiple blank lines in element children', async () => {
2803
+ const input = `export component App() {
2804
+ <div>
2805
+ <span>{'First'}</span>
2806
+
2807
+
2808
+ <span>{'Second'}</span>
2809
+ </div>
2810
+ }`;
2811
+
2812
+ const expected = `export component App() {
2813
+ <div>
2814
+ <span>{'First'}</span>
2815
+
2816
+ <span>{'Second'}</span>
2817
+ </div>
2818
+ }`;
2819
+
2820
+ const result = await format(input, { singleQuote: true });
2821
+ expect(result).toBeWithNewline(expected);
2822
+ });
2823
+ });
2824
+
2825
+ describe('Rule B: Remove leading/trailing blank lines at file and block boundaries', () => {
2826
+ it('remove all blank lines in empty statement', async () => {
2827
+ const input = `export component App() {
2828
+
2829
+
2830
+
2831
+ }`;
2832
+
2833
+ const expected = `export component App() {}`;
2834
+
2835
+ const result = await format(input, { singleQuote: true });
2836
+ expect(result).toBeWithNewline(expected);
2837
+ });
2838
+
2839
+ it('removes leading blank line at file start', async () => {
2840
+ const input = `
2841
+
2842
+ export component App() {
2843
+ let x = 1;
2844
+ }`;
2845
+
2846
+ const expected = `export component App() {
2847
+ let x = 1;
2848
+ }`;
2849
+
2850
+ const result = await format(input, { singleQuote: true });
2851
+ expect(result).toBeWithNewline(expected);
2852
+ });
2853
+
2854
+ it('removes trailing blank line at file end (preserves single newline)', async () => {
2855
+ const input = `export component App() {
2856
+ let x = 1;
2857
+ }
2858
+
2859
+ `;
2860
+
2861
+ const expected = `export component App() {
2862
+ let x = 1;
2863
+ }`;
2864
+
2865
+ const result = await format(input, { singleQuote: true });
2866
+ expect(result).toBeWithNewline(expected);
2867
+ });
2868
+
2869
+ it('removes blank lines immediately after opening brace', async () => {
2870
+ const input = `export component App() {
2871
+
2872
+ let x = 1;
2873
+ let y = 2;
2874
+ }`;
2875
+
2876
+ const expected = `export component App() {
2877
+ let x = 1;
2878
+ let y = 2;
2879
+ }`;
2880
+
2881
+ const result = await format(input, { singleQuote: true });
2882
+ expect(result).toBeWithNewline(expected);
2883
+ });
2884
+
2885
+ it('removes blank lines immediately before closing brace', async () => {
2886
+ const input = `export component App() {
2887
+ let x = 1;
2888
+ let y = 2;
2889
+
2890
+ }`;
2891
+
2892
+ const expected = `export component App() {
2893
+ let x = 1;
2894
+ let y = 2;
2895
+ }`;
2896
+
2897
+ const result = await format(input, { singleQuote: true });
2898
+ expect(result).toBeWithNewline(expected);
2899
+ });
2900
+
2901
+ it('removes leading blank line inside if block', async () => {
2902
+ const input = `export component App() {
2903
+ if (true) {
2904
+
2905
+ console.log('test');
2906
+ }
2907
+ }`;
2908
+
2909
+ const expected = `export component App() {
2910
+ if (true) {
2911
+ console.log('test');
2912
+ }
2913
+ }`;
2914
+
2915
+ const result = await format(input, { singleQuote: true });
2916
+ expect(result).toBeWithNewline(expected);
2917
+ });
2918
+
2919
+ it('removes trailing blank line inside if block', async () => {
2920
+ const input = `export component App() {
2921
+ if (true) {
2922
+ console.log('test');
2923
+
2924
+ }
2925
+ }`;
2926
+
2927
+ const expected = `export component App() {
2928
+ if (true) {
2929
+ console.log('test');
2930
+ }
2931
+ }`;
2932
+
2933
+ const result = await format(input, { singleQuote: true });
2934
+ expect(result).toBeWithNewline(expected);
2935
+ });
2936
+ });
2937
+
2938
+ describe('Rule C: Preserve internal blank lines in multi-line structures', () => {
2939
+ it('preserves blank lines between array elements when multi-line', async () => {
2940
+ const input = `export component App() {
2941
+ let arr = [
2942
+ 1,
2943
+
2944
+ 2,
2945
+
2946
+ 3
2947
+ ];
2948
+ }`;
2949
+
2950
+ const expected = `export component App() {
2951
+ let arr = [
2952
+ 1,
2953
+
2954
+ 2,
2955
+
2956
+ 3,
2957
+ ];
2958
+ }`;
2959
+
2960
+ const result = await format(input, { singleQuote: true });
2961
+ expect(result).toBeWithNewline(expected);
2962
+ });
2963
+
2964
+ it('preserves blank lines between object properties when multi-line', async () => {
2965
+ const input = `export component App() {
2966
+ let obj = {
2967
+ a: 1,
2968
+
2969
+ b: 2,
2970
+
2971
+ c: 3
2972
+ };
2973
+ }`;
2974
+
2975
+ const expected = `export component App() {
2976
+ let obj = {
2977
+ a: 1,
2978
+
2979
+ b: 2,
2980
+
2981
+ c: 3,
2982
+ };
2983
+ }`;
2984
+
2985
+ const result = await format(input, { singleQuote: true });
2986
+ expect(result).toBeWithNewline(expected);
2987
+ });
2988
+
2989
+ it('preserves blank lines between function parameters when multi-line', async () => {
2990
+ const input = `export component App() {
2991
+ function test(
2992
+ a,
2993
+
2994
+ b,
2995
+
2996
+ c
2997
+ ) {
2998
+ return a + b + c;
2999
+ }
3000
+ }`;
3001
+
3002
+ const expected = `export component App() {
3003
+ function test(
3004
+ a,
3005
+
3006
+ b,
3007
+
3008
+ c,
3009
+ ) {
3010
+ return a + b + c;
3011
+ }
3012
+ }`;
3013
+
3014
+ const result = await format(input, { singleQuote: true });
3015
+ expect(result).toBeWithNewline(expected);
3016
+ });
3017
+
3018
+ it('preserves blank lines between call arguments when multi-line', async () => {
3019
+ const input = `export component App() {
3020
+ console.log(
3021
+ 'first',
3022
+
3023
+ 'second',
3024
+
3025
+ 'third',
3026
+ );
3027
+ }`;
3028
+
3029
+ const expected = `export component App() {
3030
+ console.log(
3031
+ 'first',
3032
+
3033
+ 'second',
3034
+
3035
+ 'third',
3036
+ );
3037
+ }`;
3038
+
3039
+ const result = await format(input, { singleQuote: true });
3040
+ expect(result).toBeWithNewline(expected);
3041
+ });
3042
+
3043
+ it('preserves blank lines between JSX element children', async () => {
3044
+ const input = `export component App() {
3045
+ <div>
3046
+ <span>{'First'}</span>
3047
+
3048
+ <span>{'Second'}</span>
3049
+
3050
+ <span>{'Third'}</span>
3051
+ </div>
3052
+ }`;
3053
+
3054
+ const expected = `export component App() {
3055
+ <div>
3056
+ <span>{'First'}</span>
3057
+
3058
+ <span>{'Second'}</span>
3059
+
3060
+ <span>{'Third'}</span>
3061
+ </div>
3062
+ }`;
3063
+
3064
+ const result = await format(input, { singleQuote: true });
3065
+ expect(result).toBeWithNewline(expected);
3066
+ });
3067
+ });
3068
+
3069
+ describe('Rule D: Remove blank lines immediately after opening or before closing delimiters', () => {
3070
+ it('removes blank line immediately after opening paren in params', async () => {
3071
+ const input = `export component App() {
3072
+ function foo(
3073
+
3074
+ a,
3075
+ b
3076
+ ) {
3077
+ return a + b;
3078
+ }
3079
+ }`;
3080
+
3081
+ const expected = `export component App() {
3082
+ function foo(a, b) {
3083
+ return a + b;
3084
+ }
3085
+ }`;
3086
+
3087
+ const result = await format(input, { singleQuote: true });
3088
+ expect(result).toBeWithNewline(expected);
3089
+ });
3090
+
3091
+ it('removes blank line immediately before closing paren in params', async () => {
3092
+ const input = `export component App() {
3093
+ function foo(
3094
+ a,
3095
+ b
3096
+
3097
+ ) {
3098
+ return a + b;
3099
+ }
3100
+ }`;
3101
+
3102
+ const expected = `export component App() {
3103
+ function foo(a, b) {
3104
+ return a + b;
3105
+ }
3106
+ }`;
3107
+
3108
+ const result = await format(input, { singleQuote: true });
3109
+ expect(result).toBeWithNewline(expected);
3110
+ });
3111
+
3112
+ it('removes blank line immediately after opening paren in call', async () => {
3113
+ const input = `export component App() {
3114
+ foo(
3115
+
3116
+ 'a',
3117
+ 'b'
3118
+ );
3119
+ }`;
3120
+
3121
+ const expected = `export component App() {
3122
+ foo('a', 'b');
3123
+ }`;
3124
+
3125
+ const result = await format(input, { singleQuote: true });
3126
+ expect(result).toBeWithNewline(expected);
3127
+ });
3128
+
3129
+ it('removes blank line immediately after opening bracket in array', async () => {
3130
+ const input = `export component App() {
3131
+ let arr = [
3132
+
3133
+ 1,
3134
+ 2,
3135
+ 3
3136
+ ];
3137
+ }`;
3138
+
3139
+ const expected = `export component App() {
3140
+ let arr = [1, 2, 3];
3141
+ }`;
3142
+
3143
+ const result = await format(input, { singleQuote: true });
3144
+ expect(result).toBeWithNewline(expected);
3145
+ });
3146
+
3147
+ it('removes blank line immediately before closing bracket in array', async () => {
3148
+ const input = `export component App() {
3149
+ let arr = [
3150
+ 1,
3151
+ 2,
3152
+ 3
3153
+
3154
+ ];
3155
+ }`;
3156
+
3157
+ const expected = `export component App() {
3158
+ let arr = [1, 2, 3];
3159
+ }`;
3160
+
3161
+ const result = await format(input, { singleQuote: true });
3162
+ expect(result).toBeWithNewline(expected);
3163
+ });
3164
+
3165
+ it('removes blank line immediately after opening brace in object', async () => {
3166
+ const input = `export component App() {
3167
+ let obj = {
3168
+
3169
+ a: 1,
3170
+ b: 2
3171
+ };
3172
+ }`;
3173
+
3174
+ const expected = `export component App() {
3175
+ let obj = {
3176
+ a: 1,
3177
+ b: 2,
3178
+ };
3179
+ }`;
3180
+
3181
+ const result = await format(input, { singleQuote: true });
3182
+ expect(result).toBeWithNewline(expected);
3183
+ });
3184
+
3185
+ it('removes blank line immediately before closing brace in object', async () => {
3186
+ const input = `export component App() {
3187
+ let obj = {
3188
+ a: 1,
3189
+ b: 2
3190
+
3191
+ };
3192
+ }`;
3193
+
3194
+ const expected = `export component App() {
3195
+ let obj = {
3196
+ a: 1,
3197
+ b: 2,
3198
+ };
3199
+ }`;
3200
+
3201
+ const result = await format(input, { singleQuote: true });
3202
+ expect(result).toBeWithNewline(expected);
3203
+ });
3204
+ });
3205
+
3206
+ describe('Combined rules: preserve internal, remove leading/trailing', () => {
3207
+ it('preserves internal blank lines but removes leading/trailing in params', async () => {
3208
+ const input = `export component App() {
3209
+ function foo(
3210
+
3211
+ a,
3212
+
3213
+ b,
3214
+
3215
+ c
3216
+
3217
+ ) {
3218
+ return a + b + c;
3219
+ }
3220
+ }`;
3221
+
3222
+ const expected = `export component App() {
3223
+ function foo(
3224
+ a,
3225
+
3226
+ b,
3227
+
3228
+ c,
3229
+ ) {
3230
+ return a + b + c;
3231
+ }
3232
+ }`;
3233
+
3234
+ const result = await format(input, { singleQuote: true });
3235
+ expect(result).toBeWithNewline(expected);
3236
+ });
3237
+
3238
+ it('preserves internal blank lines but removes leading/trailing in arrays', async () => {
3239
+ const input = `export component App() {
3240
+ let arr = [
3241
+
3242
+ 1,
3243
+
3244
+ 2,
3245
+
3246
+ 3
3247
+
3248
+ ];
3249
+ }`;
3250
+
3251
+ const expected = `export component App() {
3252
+ let arr = [
3253
+ 1,
3254
+
3255
+ 2,
3256
+
3257
+ 3,
3258
+ ];
3259
+ }`;
3260
+
3261
+ const result = await format(input, { singleQuote: true });
3262
+ expect(result).toBeWithNewline(expected);
3263
+ });
3264
+
3265
+ it('preserves internal blank lines but removes leading/trailing in objects', async () => {
3266
+ const input = `export component App() {
3267
+ let obj = {
3268
+
3269
+ a: 1,
3270
+
3271
+ b: 2,
3272
+
3273
+ c: 3
3274
+
3275
+ };
3276
+ }`;
3277
+
3278
+ const expected = `export component App() {
3279
+ let obj = {
3280
+ a: 1,
3281
+
3282
+ b: 2,
3283
+
3284
+ c: 3,
3285
+ };
3286
+ }`;
3287
+
3288
+ const result = await format(input, { singleQuote: true });
3289
+ expect(result).toBeWithNewline(expected);
3290
+ });
3291
+ });
3292
+
3293
+ describe('Statement-level blank lines (should be preserved)', () => {
3294
+ it('preserves blank lines between top-level statements', async () => {
3295
+ const input = `export component App() {
3296
+ let x = 1;
3297
+
3298
+ let y = 2;
3299
+
3300
+ console.log(x, y);
3301
+ }`;
3302
+
3303
+ const expected = `export component App() {
3304
+ let x = 1;
3305
+
3306
+ let y = 2;
3307
+
3308
+ console.log(x, y);
3309
+ }`;
3310
+
3311
+ const result = await format(input, { singleQuote: true });
3312
+ expect(result).toBeWithNewline(expected);
3313
+ });
3314
+
3315
+ it('preserves blank lines between class members', async () => {
3316
+ const input = `class Foo {
3317
+ method1() {
3318
+ return 1;
3319
+ }
3320
+
3321
+ method2() {
3322
+ return 2;
3323
+ }
3324
+
3325
+ method3() {
3326
+ return 3;
3327
+ }
3328
+ }`;
3329
+
3330
+ const expected = `class Foo {
3331
+ method1() {
3332
+ return 1;
3333
+ }
3334
+
3335
+ method2() {
3336
+ return 2;
3337
+ }
3338
+
3339
+ method3() {
3340
+ return 3;
3341
+ }
3342
+ }`;
3343
+
3344
+ const result = await format(input, { singleQuote: true });
3345
+ expect(result).toBeWithNewline(expected);
3346
+ });
3347
+
3348
+ it('should keep blank line between components with a trailing comment at the end of the first', async () => {
3349
+ const expected = `component SVG({ children }) {
3350
+ <svg width={20} height={20} fill="blue" viewBox="0 0 30 10" preserveAspectRatio="none">
3351
+ let test = track(8);
3352
+ {test}
3353
+ <polygon points="0,0 30,0 15,10" />
3354
+ </svg>
3355
+ // <div><children /></div>
3356
+ }
3357
+
3358
+ component Polygon() {
3359
+ <polygon points="0,0 30,0 15,10" />
3360
+ }`;
3361
+
3362
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
3363
+ expect(result).toBeWithNewline(expected);
3364
+ });
3365
+ });
3366
+
3367
+ describe('Arrays with printWidth constraints', () => {
3368
+ it('inlines array elements when they fit within printWidth', async () => {
3369
+ const input = `export component App() {
3370
+ let arr = [1, 2, 3, 4, 5,
3371
+
3372
+ 6, 7,
3373
+
3374
+ 8];
3375
+ }`;
3376
+
3377
+ const expected = `export component App() {
3378
+ let arr = [
3379
+ 1, 2, 3, 4, 5,
3380
+
3381
+ 6, 7,
3382
+
3383
+ 8,
3384
+ ];
3385
+ }`;
3386
+
3387
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
3388
+ expect(result).toBeWithNewline(expected);
3389
+ });
3390
+
3391
+ // Note: Known limitation - fill() doesn't account for parent bracket breaking
3392
+ // With very small printWidth, bracket should break adding extra indent
3393
+ // but fill() calculates widths before knowing about parent break
3394
+ it('breaks array elements when they exceed printWidth 10', async () => {
3395
+ const input = `export component App() {
3396
+ let arr = [1, 2, 3, 4, 5,
3397
+
3398
+ 6, 7,
3399
+
3400
+ 8];
3401
+ }`;
3402
+
3403
+ // With printWidth 10, all elements break to separate lines
3404
+ // Because even "6, 7," is 5 chars + indentation = exceeds 10
3405
+ const expected = `export component App() {
3406
+ let arr =
3407
+ [
3408
+ 1,
3409
+ 2,
3410
+ 3,
3411
+ 4,
3412
+ 5,
3413
+
3414
+ 6,
3415
+ 7,
3416
+
3417
+ 8,
3418
+ ];
3419
+ }`;
3420
+
3421
+ const result = await format(input, { singleQuote: true, printWidth: 10 });
3422
+ expect(result).toBeWithNewline(expected);
3423
+ });
3424
+
3425
+ it('fits elements on same line with printWidth 11', async () => {
3426
+ const input = `export component App() {
3427
+ let arr = [1, 2, 3, 4, 5,
3428
+
3429
+ 6, 7,
3430
+
3431
+ 8];
3432
+ }`;
3433
+
3434
+ // With printWidth 11: " 6, 7," is exactly 9 chars, should fit
3435
+ const expected = `export component App() {
3436
+ let arr =
3437
+ [
3438
+ 1, 2,
3439
+ 3, 4,
3440
+ 5,
3441
+
3442
+ 6, 7,
3443
+
3444
+ 8,
3445
+ ];
3446
+ }`;
3447
+
3448
+ const result = await format(input, { singleQuote: true, printWidth: 11 });
3449
+ expect(result).toBeWithNewline(expected);
3450
+ });
3451
+
3452
+ it('fits more elements with printWidth 15', async () => {
3453
+ const input = `export component App() {
3454
+ let arr = [1, 2, 3, 4, 5,
3455
+
3456
+ 6, 7,
3457
+
3458
+ 8];
3459
+ }`;
3460
+
3461
+ // With printWidth 15: " 1, 2, 3," is 12 chars, should fit 1, 2, 3 together
3462
+ const expected = `export component App() {
3463
+ let arr = [
3464
+ 1, 2, 3, 4,
3465
+ 5,
3466
+
3467
+ 6, 7,
3468
+
3469
+ 8,
3470
+ ];
3471
+ }`;
3472
+
3473
+ const result = await format(input, { singleQuote: true, printWidth: 15 });
3474
+ expect(result).toBeWithNewline(expected);
3475
+ });
3476
+
3477
+ it('fits even more elements with printWidth 18', async () => {
3478
+ const input = `export component App() {
3479
+ let arr = [1, 2, 3, 4, 5,
3480
+
3481
+ 6, 7,
3482
+
3483
+ 8];
3484
+ }`;
3485
+
3486
+ // With printWidth 18: " 1, 2, 3, 4," is 15 chars, should fit 1, 2, 3, 4 together
3487
+ const expected = `export component App() {
3488
+ let arr = [
3489
+ 1, 2, 3, 4, 5,
3490
+
3491
+ 6, 7,
3492
+
3493
+ 8,
3494
+ ];
3495
+ }`;
3496
+
3497
+ const result = await format(input, { singleQuote: true, printWidth: 18 });
3498
+ expect(result).toBeWithNewline(expected);
3499
+ });
3500
+
3501
+ it('places each object on its own line when array contains objects where each has multiple properties', async () => {
3502
+ const input = `export component App() {
3503
+ let arr = [{ a: 1, b: 2 }, { c: 3, d: 4 }, { e: 5, f: 6 }];
3504
+ }`;
3505
+
3506
+ // Each object should be on its own line when all objects have >1 property
3507
+ const expected = `export component App() {
3508
+ let arr = [
3509
+ { a: 1, b: 2 },
3510
+ { c: 3, d: 4 },
3511
+ { e: 5, f: 6 },
3512
+ ];
3513
+ }`;
3514
+
3515
+ const result = await format(input, { singleQuote: true });
3516
+ expect(result).toBeWithNewline(expected);
3517
+ });
3518
+
3519
+ it('allows inline when array has single-property objects', async () => {
3520
+ const input = `export component App() {
3521
+ let arr = [{ a: 1 }, { b: 2 }, { c: 3 }];
3522
+ }`;
3523
+
3524
+ // Single-property objects can stay inline
3525
+ const expected = `export component App() {
3526
+ let arr = [{ a: 1 }, { b: 2 }, { c: 3 }];
3527
+ }`;
3528
+
3529
+ const result = await format(input, { singleQuote: true });
3530
+ expect(result).toBeWithNewline(expected);
3531
+ });
3532
+
3533
+ it('allows inline when array has mix of single and multi-property objects', async () => {
3534
+ const input = `export component App() {
3535
+ let arr = [{ a: 1 }, { b: 2, c: 3 }, { d: 4 }];
3536
+ }`;
3537
+
3538
+ // Mixed property counts - can stay inline (rule only applies when ALL objects have >1 property)
3539
+ const expected = `export component App() {
3540
+ let arr = [{ a: 1 }, { b: 2, c: 3 }, { d: 4 }];
3541
+ }`;
3542
+
3543
+ const result = await format(input, { singleQuote: true });
3544
+ expect(result).toBeWithNewline(expected);
3545
+ });
3546
+
3547
+ it('respects original formatting when array has mixture of inline and multi-line objects', async () => {
3548
+ const input = `export component App() {
3549
+ let arr = [{ a: 1, b: 2 }, {
3550
+ c: 3,
3551
+ d: 4
3552
+ }, { e: 5, f: 6 }];
3553
+ }`;
3554
+
3555
+ // Objects originally inline stay inline, originally multi-line stay multi-line
3556
+ // Each object on its own line because all have >1 property
3557
+ const expected = `export component App() {
3558
+ let arr = [
3559
+ { a: 1, b: 2 },
3560
+ {
3561
+ c: 3,
3562
+ d: 4,
3563
+ },
3564
+ { e: 5, f: 6 },
3565
+ ];
3566
+ }`;
3567
+
3568
+ const result = await format(input, { singleQuote: true });
3569
+ expect(result).toBeWithNewline(expected);
3570
+ });
3571
+ });
3572
+
3573
+ describe('<tsx:react>', () => {
3574
+ it('should format JSX inside <tsx:react> tags', async () => {
3575
+ const input = `component App() {
3576
+ <div>
3577
+ <h1>{"Hello, from Ripple!"}</h1>
3578
+ <tsx:react>
3579
+ <div className="123">Welcome from React!</div>
3580
+ </tsx:react>
3581
+ </div>
3582
+ }`;
3583
+
3584
+ const expected = `component App() {
3585
+ <div>
3586
+ <h1>{'Hello, from Ripple!'}</h1>
3587
+ <tsx:react>
3588
+ <div className="123">Welcome from React!</div>
3589
+ </tsx:react>
3590
+ </div>
3591
+ }`;
3592
+
3593
+ const result = await format(input, { singleQuote: true });
3594
+ expect(result).toBeWithNewline(expected);
3595
+ });
3596
+
3597
+ it('should format JSXFragment inside <tsx:react> tags', async () => {
3598
+ const input = `component App() {
3599
+ <div>
3600
+ <h1>{"Hello, from Ripple!"}</h1>
3601
+ <tsx:react>
3602
+ <div className="123">
3603
+ <>
3604
+ Text content
3605
+ </>
3606
+ </div>
3607
+ </tsx:react>
3608
+ </div>
3609
+ }`;
3610
+
3611
+ const expected = `component App() {
3612
+ <div>
3613
+ <h1>{'Hello, from Ripple!'}</h1>
3614
+ <tsx:react>
3615
+ <div className="123">
3616
+ <>Text content</>
3617
+ </div>
3618
+ </tsx:react>
3619
+ </div>
3620
+ }`;
3621
+
3622
+ const result = await format(input, { singleQuote: true });
3623
+ expect(result).toBeWithNewline(expected);
3624
+ });
3625
+
3626
+ it('should format empty JSXFragment', async () => {
3627
+ const input = `component App() {
3628
+ <tsx:react>
3629
+ <div>
3630
+ <></>
3631
+ </div>
3632
+ </tsx:react>
3633
+ }`;
3634
+
3635
+ const expected = `component App() {
3636
+ <tsx:react>
3637
+ <div><></></div>
3638
+ </tsx:react>
3639
+ }`;
3640
+
3641
+ const result = await format(input, { singleQuote: true });
3642
+ expect(result).toBeWithNewline(expected);
3643
+ });
3644
+
3645
+ it('should format JSXFragment with multiple children', async () => {
3646
+ const input = `component App() {
3647
+ <tsx:react>
3648
+ <>
3649
+ <div>First</div>
3650
+ <div>Second</div>
3651
+ <span>Third</span>
3652
+ </>
3653
+ </tsx:react>
3654
+ }`;
3655
+
3656
+ const expected = `component App() {
3657
+ <tsx:react>
3658
+ <>
3659
+ <div>First</div>
3660
+ <div>Second</div>
3661
+ <span>Third</span>
3662
+ </>
3663
+ </tsx:react>
3664
+ }`;
3665
+
3666
+ const result = await format(input, { singleQuote: true });
3667
+ expect(result).toBeWithNewline(expected);
3668
+ });
3669
+
3670
+ it('should format nested JSXFragments', async () => {
3671
+ const input = `component App() {
3672
+ <tsx:react>
3673
+ <div className="wrapper">
3674
+ <>
3675
+ <span>Outer fragment</span>
3676
+ <>
3677
+ <span>Inner fragment</span>
3678
+ </>
3679
+ </>
3680
+ </div>
3681
+ </tsx:react>
3682
+ }`;
3683
+
3684
+ const expected = `component App() {
3685
+ <tsx:react>
3686
+ <div className="wrapper">
3687
+ <>
3688
+ <span>Outer fragment</span>
3689
+ <>
3690
+ <span>Inner fragment</span>
3691
+ </>
3692
+ </>
3693
+ </div>
3694
+ </tsx:react>
3695
+ }`;
3696
+
3697
+ const result = await format(input, { singleQuote: true });
3698
+ expect(result).toBeWithNewline(expected);
3699
+ });
3700
+
3701
+ it('should format JSXFragment with text and elements mixed', async () => {
3702
+ const input = `component App() {
3703
+ <tsx:react>
3704
+ <>
3705
+ Some text before
3706
+ <div>Element in middle</div>
3707
+ Some text after
3708
+ </>
3709
+ </tsx:react>
3710
+ }`;
3711
+
3712
+ const expected = `component App() {
3713
+ <tsx:react>
3714
+ <>
3715
+ Some text before
3716
+ <div>Element in middle</div>
3717
+ Some text after
3718
+ </>
3719
+ </tsx:react>
3720
+ }`;
3721
+
3722
+ const result = await format(input, { singleQuote: true });
3723
+ expect(result).toBeWithNewline(expected);
3724
+ });
3725
+
3726
+ it('should format JSXFragment with expressions', async () => {
3727
+ const input = `component App() {
3728
+ <tsx:react>
3729
+ <>
3730
+ {value}
3731
+ <span>Text</span>
3732
+ {otherValue}
3733
+ </>
3734
+ </tsx:react>
3735
+ }`;
3736
+
3737
+ const expected = `component App() {
3738
+ <tsx:react>
3739
+ <>
3740
+ {value}
3741
+ <span>Text</span>
3742
+ {otherValue}
3743
+ </>
3744
+ </tsx:react>
3745
+ }`;
3746
+
3747
+ const result = await format(input, { singleQuote: true });
3748
+ expect(result).toBeWithNewline(expected);
3749
+ });
3750
+
3751
+ it('should preserve @ symbol in JSX attributes inside <tsx:react>', async () => {
3752
+ const input = `component App() {
3753
+ const count = track(0);
3754
+
3755
+ <div>
3756
+ <h1>{'Hello, from Ripple!'}</h1>
3757
+ <tsx:react>
3758
+ <Counter count={@count} />
3759
+ </tsx:react>
3760
+ </div>
3761
+ }`;
3762
+
3763
+ const expected = `component App() {
3764
+ const count = track(0);
3765
+
3766
+ <div>
3767
+ <h1>{'Hello, from Ripple!'}</h1>
3768
+ <tsx:react>
3769
+ <Counter count={@count} />
3770
+ </tsx:react>
3771
+ </div>
3772
+ }`;
3773
+
3774
+ const result = await format(input, { singleQuote: true });
3775
+ expect(result).toBeWithNewline(expected);
3776
+ });
3777
+ });
3778
+ });
3779
+ });