@tsrx/prettier-plugin 0.3.58 → 0.3.60

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.58",
3
+ "version": "0.3.60",
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.8"
30
+ "@tsrx/core": "0.1.10"
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
 
@@ -4286,23 +4286,17 @@ function printTSInterfaceBody(node, path, options, print) {
4286
4286
  * @param {AstPath<AST.TSTypeAliasDeclaration>} path - The AST path
4287
4287
  * @param {RippleFormatOptions} options - Prettier options
4288
4288
  * @param {PrintFn} print - Print callback
4289
- * @returns {Doc[]}
4289
+ * @returns {Doc}
4290
4290
  */
4291
4291
  function printTSTypeAliasDeclaration(node, path, options, print) {
4292
4292
  /** @type {Doc[]} */
4293
- const parts = [];
4294
- parts.push('type ');
4295
- parts.push(node.id.name);
4293
+ const head = ['type ', node.id.name];
4296
4294
 
4297
4295
  if (node.typeParameters) {
4298
- parts.push(path.call(print, 'typeParameters'));
4296
+ head.push(path.call(print, 'typeParameters'));
4299
4297
  }
4300
4298
 
4301
- parts.push(' = ');
4302
- parts.push(path.call(print, 'typeAnnotation'));
4303
- parts.push(semi(options));
4304
-
4305
- return parts;
4299
+ return group([head, ' =', indent([line, path.call(print, 'typeAnnotation')]), semi(options)]);
4306
4300
  }
4307
4301
 
4308
4302
  /**
@@ -5394,19 +5388,22 @@ function printTSConstructorType(node, path, options, print) {
5394
5388
  * @param {AstPath<AST.TSConditionalType>} path - The AST path
5395
5389
  * @param {RippleFormatOptions} options - Prettier options
5396
5390
  * @param {PrintFn} print - Print callback
5397
- * @returns {Doc[]}
5391
+ * @returns {Doc}
5398
5392
  */
5399
5393
  function printTSConditionalType(node, path, options, print) {
5400
- /** @type {Doc[]} */
5401
- const parts = [];
5402
- parts.push(path.call(print, 'checkType'));
5403
- parts.push(' extends ');
5404
- parts.push(path.call(print, 'extendsType'));
5405
- parts.push(' ? ');
5406
- parts.push(path.call(print, 'trueType'));
5407
- parts.push(' : ');
5408
- parts.push(path.call(print, 'falseType'));
5409
- return parts;
5394
+ const trueType = path.call(print, 'trueType');
5395
+ const falseType = path.call(print, 'falseType');
5396
+
5397
+ const shouldIndentTrueType = node.trueType.type !== 'TSConditionalType';
5398
+ const shouldIndentFalseType = node.falseType.type !== 'TSConditionalType';
5399
+
5400
+ return group([
5401
+ path.call(print, 'checkType'),
5402
+ ' extends ',
5403
+ path.call(print, 'extendsType'),
5404
+ indent([line, '? ', shouldIndentTrueType ? indent(trueType) : trueType]),
5405
+ indent([line, ': ', shouldIndentFalseType ? indent(falseType) : falseType]),
5406
+ ]);
5410
5407
  }
5411
5408
 
5412
5409
  /**
@@ -5504,6 +5501,27 @@ function printTSIndexedAccessType(node, path, options, print) {
5504
5501
  return [path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']'];
5505
5502
  }
5506
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
+
5507
5525
  /**
5508
5526
  * @param {AST.Node} parentNode
5509
5527
  * @param {AST.Node} firstChild
@@ -6273,6 +6291,8 @@ function printElement(element, path, options, print) {
6273
6291
  }, 'attributes')
6274
6292
  : [];
6275
6293
  const shouldForceBreak = hasOpeningTagComments || hasBreakingAttribute;
6294
+ const openingTagAlwaysBreaks =
6295
+ (hasAttributes && options.singleAttributePerLine) || shouldForceBreak;
6276
6296
  const openingTag = group([
6277
6297
  '<',
6278
6298
  tagName,
@@ -6384,12 +6404,23 @@ function printElement(element, path, options, print) {
6384
6404
  isTextLikeChild && Array.isArray(currentChild.expression?.leadingComments)
6385
6405
  ? currentChild.expression.leadingComments
6386
6406
  : null;
6407
+ const elementBodyLeadingComments =
6408
+ hasTextLeadingComments && node.openingElement
6409
+ ? /** @type {AST.Comment[]} */ (currentChild.leadingComments).filter(
6410
+ (comment) =>
6411
+ comment.context?.containerId === node.metadata?.commentContainerId &&
6412
+ comment.context?.beforeMeaningfulChild &&
6413
+ typeof comment.start === 'number' &&
6414
+ comment.start >= /** @type {AST.NodeWithLocation} */ (node.openingElement).end &&
6415
+ comment.start < /** @type {AST.NodeWithLocation} */ (currentChild).start,
6416
+ )
6417
+ : [];
6387
6418
 
6388
6419
  if (hasTextLeadingComments) {
6389
6420
  for (let j = 0; j < /** @type {AST.Comment[]} */ (currentChild.leadingComments).length; j++) {
6390
6421
  const comment = /** @type {AST.Comment[]} */ (currentChild.leadingComments)[j];
6391
6422
  // Don't lift comments that belong inside the opening tag (handled in attribute section)
6392
- if (!openingTagCommentsSet.has(comment)) {
6423
+ if (!openingTagCommentsSet.has(comment) && !elementBodyLeadingComments.includes(comment)) {
6393
6424
  fallbackElementComments.push(comment);
6394
6425
  }
6395
6426
  }
@@ -6408,10 +6439,15 @@ function printElement(element, path, options, print) {
6408
6439
  ? path.call((childPath) => print(childPath, childPrintArgs), 'children', i)
6409
6440
  : path.call(print, 'children', i);
6410
6441
 
6411
- const childDoc =
6412
- rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
6413
- ? [...createElementLevelCommentParts(rawExpressionLeadingComments), printedChild]
6414
- : printedChild;
6442
+ const childLeadingCommentParts =
6443
+ elementBodyLeadingComments.length > 0
6444
+ ? createElementLevelCommentParts(elementBodyLeadingComments)
6445
+ : rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
6446
+ ? createElementLevelCommentParts(rawExpressionLeadingComments)
6447
+ : null;
6448
+ const childDoc = childLeadingCommentParts
6449
+ ? [...childLeadingCommentParts, printedChild]
6450
+ : printedChild;
6415
6451
  finalChildren.push(childDoc);
6416
6452
 
6417
6453
  // Insert element-body comments that fall between this child and the next child (or the closing tag).
@@ -6538,9 +6574,19 @@ function printElement(element, path, options, print) {
6538
6574
  const isNonSelfClosingElement =
6539
6575
  firstChild && firstChild.type === 'Element' && !firstChild.selfClosing;
6540
6576
  const isElementChild = firstChild && firstChild.type === 'Element';
6577
+ const isRawTextChild =
6578
+ firstChild && firstChild.type === 'Text' && typeof firstChild.raw === 'string';
6541
6579
 
6542
- if (typeof child === 'string' && shouldInlineSingleChild(node, firstChild, child)) {
6543
- 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
+ ]);
6544
6590
  } else if (
6545
6591
  child &&
6546
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';
@@ -3474,6 +3642,20 @@ const items = [] as unknown[];`;
3474
3642
  expect(result).toBeWithNewline(expected);
3475
3643
  });
3476
3644
 
3645
+ it('should break long nested TypeScript conditional type aliases', async () => {
3646
+ const input = `type PageModelValue<Value> = Value extends ReadonlySignal<unknown> ? Value : Value extends (...args: any[]) => any ? Value : Value extends object ? { [Key in keyof Value]: PageModelValue<Value[Key]> } : never;`;
3647
+ const expected = `type PageModelValue<Value> =
3648
+ Value extends ReadonlySignal<unknown>
3649
+ ? Value
3650
+ : Value extends (...args: any[]) => any
3651
+ ? Value
3652
+ : Value extends object
3653
+ ? { [Key in keyof Value]: PageModelValue<Value[Key]> }
3654
+ : never;`;
3655
+ const result = await format(input);
3656
+ expect(result).toBeWithNewline(expected);
3657
+ });
3658
+
3477
3659
  it('should format TypeScript mapped types (TSMappedType)', async () => {
3478
3660
  const input = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] }`;
3479
3661
  const expected = `type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] };`;