@tsrx/prettier-plugin 0.3.59 → 0.3.61

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.59",
3
+ "version": "0.3.61",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -27,7 +27,7 @@
27
27
  "prettier": "^3.8.3"
28
28
  },
29
29
  "dependencies": {
30
- "@tsrx/core": "0.1.9"
30
+ "@tsrx/core": "0.1.11"
31
31
  },
32
32
  "files": [
33
33
  "src/"
package/src/index.js CHANGED
@@ -36,7 +36,7 @@ const {
36
36
  indentIfBreak,
37
37
  lineSuffix,
38
38
  } = builders;
39
- const { willBreak } = utils;
39
+ const { replaceEndOfLine, willBreak } = utils;
40
40
 
41
41
  /** @type {import('prettier').Plugin['languages']} */
42
42
  export const languages = [
@@ -2236,7 +2236,7 @@ function printRippleNode(node, path, options, print, args) {
2236
2236
 
2237
2237
  case 'Text': {
2238
2238
  if (typeof node.raw === 'string') {
2239
- nodeContent = node.raw;
2239
+ nodeContent = printRawText(node.raw);
2240
2240
  break;
2241
2241
  }
2242
2242
 
@@ -5501,6 +5501,27 @@ function printTSIndexedAccessType(node, path, options, print) {
5501
5501
  return [path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']'];
5502
5502
  }
5503
5503
 
5504
+ /**
5505
+ * Print direct TSRX text so it can wrap like JSX text when an element body breaks.
5506
+ * @param {string} raw
5507
+ * @returns {Doc}
5508
+ */
5509
+ function printRawText(raw) {
5510
+ const text = raw.trim().replace(/(?:\r\n|\r|\n)[^\S\r\n]+/gu, ' ');
5511
+ if (!text) {
5512
+ return '';
5513
+ }
5514
+
5515
+ return fill(
5516
+ text
5517
+ .split(/([^\S\r\n]+)/u)
5518
+ .filter(Boolean)
5519
+ .map((part) => {
5520
+ return /^[^\S\r\n]+$/u.test(part) ? line : replaceEndOfLine(part);
5521
+ }),
5522
+ );
5523
+ }
5524
+
5504
5525
  /**
5505
5526
  * @param {AST.Node} parentNode
5506
5527
  * @param {AST.Node} firstChild
@@ -6270,6 +6291,8 @@ function printElement(element, path, options, print) {
6270
6291
  }, 'attributes')
6271
6292
  : [];
6272
6293
  const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
6294
+ const openingTagAlwaysBreaks =
6295
+ (hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
6273
6296
  const openingTag = group([
6274
6297
  '<',
6275
6298
  tagName,
@@ -6551,9 +6574,19 @@ function printElement(element, path, options, print) {
6551
6574
  const isNonSelfClosingElement =
6552
6575
  firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6553
6576
  const isElementChild = firstChild && firstChild.type === 'Element';
6577
+ const isRawTextChild =
6578
+ firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
6554
6579
 
6555
- if (typeof child === 'string' && shouldInlineSingleChild(node, firstChild, child)) {
6556
- elementOutput = group([openingTag, child, closingTag]);
6580
+ if (
6581
+ (typeof child === 'string' || isRawTextChild) &&
6582
+ shouldInlineSingleChild(node, firstChild, child)
6583
+ ) {
6584
+ elementOutput = openingTagAlwaysBreaks
6585
+ ? [openingTag, indent([hardline, child]), hardline, closingTag]
6586
+ : conditionalGroup([
6587
+ group([openingTag, child, closingTag]),
6588
+ [openingTag, indent([hardline, child]), hardline, closingTag],
6589
+ ]);
6557
6590
  } else if (
6558
6591
  child &&
6559
6592
  typeof child === 'object' &&
package/src/index.test.js CHANGED
@@ -4,6 +4,14 @@ import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import { languages } from './index.js';
6
6
 
7
+ /**
8
+ * @typedef {typeof prettier & {
9
+ * __debug: {
10
+ * printToDoc: (code: string, options: import('prettier').Options) => Promise<import('prettier').Doc>;
11
+ * };
12
+ * }} PrettierWithDebug
13
+ */
14
+
7
15
  const __filename = fileURLToPath(import.meta.url);
8
16
  const __dirname = dirname(__filename);
9
17
 
@@ -62,6 +70,41 @@ describe('prettier-plugin', () => {
62
70
  });
63
71
  };
64
72
 
73
+ /**
74
+ * @param {string} code
75
+ * @param {import('prettier').Options} [options]
76
+ */
77
+ const printToDoc = async (code, options = {}) => {
78
+ const prettierWithDebug = /** @type {PrettierWithDebug} */ (prettier);
79
+ return await prettierWithDebug.__debug.printToDoc(code, {
80
+ parser: 'ripple',
81
+ plugins: [join(__dirname, 'index.js')],
82
+ ...options,
83
+ });
84
+ };
85
+
86
+ /**
87
+ * @param {unknown} doc
88
+ * @param {(doc: Record<string, unknown>) => boolean} predicate
89
+ * @returns {boolean}
90
+ */
91
+ const docContains = (doc, predicate) => {
92
+ if (!doc || typeof doc !== 'object') {
93
+ return false;
94
+ }
95
+
96
+ if (Array.isArray(doc)) {
97
+ return doc.some((part) => docContains(part, predicate));
98
+ }
99
+
100
+ const objectDoc = /** @type {Record<string, unknown>} */ (doc);
101
+ if (predicate(objectDoc)) {
102
+ return true;
103
+ }
104
+
105
+ return Object.values(objectDoc).some((value) => docContains(value, predicate));
106
+ };
107
+
65
108
  /**
66
109
  * @param {string} code
67
110
  * @param {Partial<import('prettier').CursorOptions>} options
@@ -1055,6 +1098,23 @@ export component Test({ a, b }: Props) {}`;
1055
1098
  expect(result).toBeWithNewline(expected);
1056
1099
  });
1057
1100
 
1101
+ it('should not force attribute-less elements to break with singleAttributePerLine', async () => {
1102
+ const input = `component One() {
1103
+ <div>"Hello"</div>
1104
+ }`;
1105
+
1106
+ const expected = `component One() {
1107
+ <div>"Hello"</div>
1108
+ }`;
1109
+
1110
+ const result = await format(input, {
1111
+ singleQuote: true,
1112
+ printWidth: 100,
1113
+ singleAttributePerLine: true,
1114
+ });
1115
+ expect(result).toBeWithNewline(expected);
1116
+ });
1117
+
1058
1118
  it('should respect singleAttributePerLine set to false setting', async () => {
1059
1119
  const input = `component One() {
1060
1120
  <button
@@ -3239,6 +3299,114 @@ const items = [] as unknown[];`;
3239
3299
  expect(result).toBeWithNewline(expected);
3240
3300
  });
3241
3301
 
3302
+ it('should preserve literal newlines in direct double-quoted text children', async () => {
3303
+ const input = `component App() {
3304
+ <pre>"first
3305
+ second"</pre>
3306
+ }`;
3307
+
3308
+ const expected = `component App() {
3309
+ <pre>"first
3310
+ second"</pre>
3311
+ }`;
3312
+
3313
+ const result = await format(input);
3314
+ expect(result).toBeWithNewline(expected);
3315
+ });
3316
+
3317
+ it('should wrap direct double-quoted text children idempotently', async () => {
3318
+ const input = `component App() {
3319
+ <p class="lede">
3320
+ "Set up TSRX with React, Preact, Solid, Vue, or Ripple and then wire in the editor tooling that makes "
3321
+ <code class="inline-code">".tsrx"</code>
3322
+ " files feel native in the rest of your repo."
3323
+ </p>
3324
+ }`;
3325
+
3326
+ const expected = `component App() {
3327
+ <p class="lede">
3328
+ "Set up TSRX with React, Preact, Solid, Vue, or Ripple and then wire in the editor tooling that
3329
+ makes "
3330
+ <code class="inline-code">".tsrx"</code>
3331
+ " files feel native in the rest of your repo."
3332
+ </p>
3333
+ }`;
3334
+
3335
+ const result = await format(input, { printWidth: 100 });
3336
+ const secondResult = await format(result, { printWidth: 100 });
3337
+ expect(result).toBeWithNewline(expected);
3338
+ expect(secondResult).toBeWithNewline(expected);
3339
+ });
3340
+
3341
+ it('should break long direct text children after inline attributes', async () => {
3342
+ const input = `component App() {
3343
+ <span
3344
+ class={styles.notificationMessage}
3345
+ >"The report is ready. Review the summary before sharing it with the team."</span>
3346
+ }`;
3347
+
3348
+ const expected = `component App() {
3349
+ <span class={styles.notificationMessage}>
3350
+ "The report is ready. Review the summary before sharing it with the team."
3351
+ </span>
3352
+ }`;
3353
+
3354
+ const result = await format(input, { printWidth: 80 });
3355
+ expect(result).toBeWithNewline(expected);
3356
+ });
3357
+
3358
+ it('should wrap long direct text children when elements break', async () => {
3359
+ const input = `component App() {
3360
+ <span class={styles.notificationMessage}>"The report is ready. Review the summary before sharing it with the team."</span>
3361
+ }`;
3362
+
3363
+ const expectedPrintWidth70 = `component App() {
3364
+ <span class={styles.notificationMessage}>
3365
+ "The report is ready. Review the summary before sharing it with
3366
+ the team."
3367
+ </span>
3368
+ }`;
3369
+ const expectedPrintWidth40 = `component App() {
3370
+ <span
3371
+ class={styles.notificationMessage}
3372
+ >
3373
+ "The report is ready. Review the
3374
+ summary before sharing it with the
3375
+ team."
3376
+ </span>
3377
+ }`;
3378
+
3379
+ const resultPrintWidth70 = await format(input, { printWidth: 70 });
3380
+ expect(resultPrintWidth70).toBeWithNewline(expectedPrintWidth70);
3381
+
3382
+ const resultPrintWidth40 = await format(input, { printWidth: 40 });
3383
+ expect(resultPrintWidth40).toBeWithNewline(expectedPrintWidth40);
3384
+ });
3385
+
3386
+ it('should keep direct text children on the text-specific conditional layout path', async () => {
3387
+ const input = `component App() {
3388
+ <div>"The report is ready. Review the summary before sharing it with the team."</div>
3389
+ }`;
3390
+
3391
+ const doc = await printToDoc(input, { printWidth: 70 });
3392
+ const hasConditionalTextGroup = docContains(doc, (part) => {
3393
+ return (
3394
+ part.type === 'group' &&
3395
+ Array.isArray(part.expandedStates) &&
3396
+ docContains(part.contents, (childPart) => {
3397
+ return (
3398
+ childPart.type === 'fill' &&
3399
+ Array.isArray(childPart.parts) &&
3400
+ childPart.parts.some((part) => Array.isArray(part) && part.includes('"The')) &&
3401
+ childPart.parts.some((part) => Array.isArray(part) && part.includes('team."'))
3402
+ );
3403
+ })
3404
+ );
3405
+ });
3406
+
3407
+ expect(hasConditionalTextGroup).toBe(true);
3408
+ });
3409
+
3242
3410
  it('should not insert a new line between js and jsx if not provided', async () => {
3243
3411
  const expected = `export component App() {
3244
3412
  let text = 'something';