@tsrx/prettier-plugin 0.3.103 → 0.3.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsrx/prettier-plugin",
3
- "version": "0.3.103",
3
+ "version": "0.3.104",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
package/src/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { parseModule } from '@tsrx/core';
22
22
  import { doc } from 'prettier';
23
+ import postcssPlugin from 'prettier/parser-postcss.js';
23
24
 
24
25
  const { builders, utils } = doc;
25
26
  const {
@@ -51,6 +52,9 @@ export const languages = [
51
52
 
52
53
  /** @type {import('prettier').Plugin['parsers']} */
53
54
  export const parsers = {
55
+ // Carry the embedded stylesheet parsers with the TSRX plugin so browser
56
+ // consumers of prettier/standalone do not need to register PostCSS separately.
57
+ ...postcssPlugin.parsers,
54
58
  tsrx: {
55
59
  astFormat: 'ripple-ast',
56
60
  /**
@@ -82,6 +86,7 @@ export const parsers = {
82
86
 
83
87
  /** @type {import('prettier').Plugin['printers']} */
84
88
  export const printers = {
89
+ ...postcssPlugin.printers,
85
90
  'ripple-ast': {
86
91
  /**
87
92
  * @param {AstPath<AST.Node | AST.CSS.StyleSheet>} path
@@ -123,8 +128,8 @@ export const printers = {
123
128
  return body;
124
129
  } catch {
125
130
  // A stylesheet that doesn't parse (e.g. mid-edit code) is an expected
126
- // state, not an error: keep it verbatim and stay quiet.
127
- return node.source;
131
+ // state, not an error: keep its authored lines and stay quiet.
132
+ return replaceEndOfLine(node.source.trim());
128
133
  }
129
134
  };
130
135
  }
@@ -1664,7 +1669,8 @@ function printRippleNode(node, path, options, print, args) {
1664
1669
  if (!node.source || !node.source.trim()) {
1665
1670
  nodeContent = '';
1666
1671
  } else {
1667
- nodeContent = node.source.trim();
1672
+ // Preserve authored lines when embedded-language formatting is disabled.
1673
+ nodeContent = replaceEndOfLine(node.source.trim());
1668
1674
  }
1669
1675
  break;
1670
1676
  }
@@ -5820,10 +5826,10 @@ function isSimpleJSXExpressionChild(child) {
5820
5826
  expression?.type === 'Identifier' ||
5821
5827
  expression?.type === 'Literal' ||
5822
5828
  expression?.type === 'TemplateLiteral' ||
5823
- // Stock Prettier keeps a single `{expr}` child inline regardless of the
5824
- // expression kind (member access, calls, etc.); only multiple children break.
5825
5829
  expression?.type === 'MemberExpression' ||
5826
- expression?.type === 'CallExpression'
5830
+ expression?.type === 'CallExpression' ||
5831
+ expression?.type === 'BinaryExpression' ||
5832
+ expression?.type === 'LogicalExpression'
5827
5833
  );
5828
5834
  }
5829
5835
 
@@ -6091,6 +6097,21 @@ function printJSXElement(node, path, options, print) {
6091
6097
  (child) => child.type !== 'JSXText' || child.value.trim(),
6092
6098
  );
6093
6099
  const singleMeaningfulChild = meaningfulChildren.length === 1 ? meaningfulChildren[0] : null;
6100
+ const singleExpression =
6101
+ singleMeaningfulChild?.type === 'JSXExpressionContainer'
6102
+ ? singleMeaningfulChild.expression
6103
+ : null;
6104
+ if (
6105
+ !forceMultiline &&
6106
+ childrenDocs.length === 1 &&
6107
+ (singleExpression?.type === 'BinaryExpression' ||
6108
+ singleExpression?.type === 'LogicalExpression')
6109
+ ) {
6110
+ // Keep a short operation against its tags, but give a wrapping operation
6111
+ // an indented element body instead of aligning continuations after `{`.
6112
+ // Group the opening tag with that body so wrapped attributes break both.
6113
+ return group([openingTag, indent([softline, childrenDocs[0]]), softline, '</', tagName, '>']);
6114
+ }
6094
6115
  if (
6095
6116
  !forceMultiline &&
6096
6117
  childrenDocs.length === 1 &&
package/src/index.test.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import prettier from 'prettier';
3
+ import standalonePrettier from 'prettier/standalone';
4
+ import estreePlugin from 'prettier/plugins/estree';
3
5
  import { fileURLToPath } from 'url';
4
6
  import { dirname, join } from 'path';
5
- import { languages, parsers } from './index.js';
7
+ import { languages, parsers, printers } from './index.js';
6
8
 
7
9
  const __filename = fileURLToPath(import.meta.url);
8
10
  const __dirname = dirname(__filename);
@@ -292,6 +294,79 @@ const items=[1,2,3];
292
294
  expect(result).toBeWithNewline(expected);
293
295
  });
294
296
 
297
+ it('formats style tags with standalone Prettier', async () => {
298
+ const input = `export default function App() @{
299
+ <style>
300
+ .demo { display: grid; gap: 0.5rem; justify-items: start; }
301
+ button { padding: 0.4rem 0.9rem; color: inherit; }
302
+ </style>
303
+ }`;
304
+ const expected = `export default function App() @{
305
+ <style>
306
+ .demo {
307
+ display: grid;
308
+ gap: 0.5rem;
309
+ justify-items: start;
310
+ }
311
+ button {
312
+ padding: 0.4rem 0.9rem;
313
+ color: inherit;
314
+ }
315
+ </style>
316
+ }`;
317
+ const options = {
318
+ parser: 'tsrx',
319
+ plugins: [{ languages, parsers, printers }, estreePlugin],
320
+ useTabs: true,
321
+ tabWidth: 2,
322
+ singleQuote: true,
323
+ printWidth: 100,
324
+ };
325
+
326
+ const result = await standalonePrettier.format(input, options);
327
+ expect(result).toBeWithNewline(expected);
328
+ expect(await standalonePrettier.format(result, options)).toBe(result);
329
+ });
330
+
331
+ it('preserves style lines when embedded formatting is disabled', async () => {
332
+ const input = `export default function App() @{
333
+ <style>
334
+ .demo {
335
+ display: grid;
336
+ gap: 0.5rem;
337
+ }
338
+ button {
339
+ color: inherit;
340
+ }
341
+ </style>
342
+ }`;
343
+
344
+ const options = {
345
+ useTabs: true,
346
+ singleQuote: true,
347
+ embeddedLanguageFormatting: 'off',
348
+ };
349
+ const result = await format(input, options);
350
+
351
+ expect(result).toBeWithNewline(input);
352
+ expect(await format(result, options)).toBe(result);
353
+ });
354
+
355
+ it('preserves style lines when embedded formatting fails', async () => {
356
+ const input = `export default function App() @{
357
+ <style>
358
+ .demo {
359
+ color red;
360
+ }
361
+ </style>
362
+ }`;
363
+ const options = { useTabs: true, singleQuote: true };
364
+ const result = await format(input, options);
365
+
366
+ expect(result).toBeWithNewline(input);
367
+ expect(await format(result, options)).toBe(result);
368
+ });
369
+
295
370
  it('formats setup statements before the TSRX return', async () => {
296
371
  const input = `function Counter(){let count=track(0);const increment=()=>count++;return <button onClick={increment}>{count}</button>}`;
297
372
  const expected = `function Counter() {
@@ -326,6 +401,51 @@ const items=[1,2,3];
326
401
  expect(result).toBeWithNewline(expected);
327
402
  });
328
403
 
404
+ it('keeps a lone binary expression child inline when it fits', async () => {
405
+ const input = `export function App() @{
406
+ <h2>{'Count: ' + count}</h2>
407
+ }`;
408
+ const expected = `export function App() @{
409
+ <h2>{'Count: ' + count}</h2>
410
+ }`;
411
+
412
+ const result = await format(input, { singleQuote: true });
413
+ expect(result).toBeWithNewline(expected);
414
+ });
415
+
416
+ it('indents a lone binary expression child when it wraps', async () => {
417
+ const input = `export function App() @{
418
+ <h2>{firstLongIdentifier + secondLongIdentifier + thirdLongIdentifier}</h2>
419
+ }`;
420
+ const expected = `export function App() @{
421
+ <h2>
422
+ {firstLongIdentifier +
423
+ secondLongIdentifier +
424
+ thirdLongIdentifier}
425
+ </h2>
426
+ }`;
427
+
428
+ const result = await format(input, { printWidth: 40 });
429
+ expect(result).toBeWithNewline(expected);
430
+ });
431
+
432
+ it('indents a lone binary expression child after wrapped attributes', async () => {
433
+ const input = `export function App() @{
434
+ <h2 firstLongAttributeName={firstLongAttributeValue} secondLongAttributeName={secondLongAttributeValue}>{a + b}</h2>
435
+ }`;
436
+ const expected = `export function App() @{
437
+ <h2
438
+ firstLongAttributeName={firstLongAttributeValue}
439
+ secondLongAttributeName={secondLongAttributeValue}
440
+ >
441
+ {a + b}
442
+ </h2>
443
+ }`;
444
+
445
+ const result = await format(input, { printWidth: 50 });
446
+ expect(result).toBeWithNewline(expected);
447
+ });
448
+
329
449
  it('preserves authored multiline whitespace around a single JSXText child', async () => {
330
450
  const input = `function Foo() @{
331
451
  @if (props.onRemove) {
@@ -5360,9 +5480,7 @@ const foo = <><Bar {...props} /></>;`;
5360
5480
  const expected = `export function Test() {
5361
5481
  const a = 1
5362
5482
  const b = 2
5363
- <div>
5364
- {a + b}
5365
- </div>
5483
+ <div>{a + b}</div>
5366
5484
  }`;
5367
5485
  const result = await format(input, { singleQuote: true, semi: false });
5368
5486
  expect(result).toBeWithNewline(expected);
@@ -5377,9 +5495,7 @@ const foo = <><Bar {...props} /></>;`;
5377
5495
  const expected = `export function Test() {
5378
5496
  const a = 1;
5379
5497
  const b = 2;
5380
- <div>
5381
- {a + b}
5382
- </div>
5498
+ <div>{a + b}</div>
5383
5499
  }`;
5384
5500
  const result = await format(input, { singleQuote: true, semi: true });
5385
5501
  expect(result).toBeWithNewline(expected);