@tsrx/prettier-plugin 0.3.93 → 0.3.95

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.93",
3
+ "version": "0.3.95",
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.4"
28
28
  },
29
29
  "dependencies": {
30
- "@tsrx/core": "0.1.37"
30
+ "@tsrx/core": "0.1.38"
31
31
  },
32
32
  "files": [
33
33
  "src/"
package/src/index.js CHANGED
@@ -37,7 +37,7 @@ const {
37
37
  lineSuffix,
38
38
  align,
39
39
  } = builders;
40
- const { replaceEndOfLine, willBreak } = utils;
40
+ const { replaceEndOfLine, stripTrailingHardline, willBreak } = utils;
41
41
 
42
42
  /** @type {import('prettier').Plugin['languages']} */
43
43
  export const languages = [
@@ -121,14 +121,38 @@ export const printers = {
121
121
  // Return the formatted CSS
122
122
  // Note: printElement will wrap this in indent(), so we don't add indent here
123
123
  return body;
124
- } catch (error) {
125
- // If CSS has syntax errors, return original unformatted content
126
- console.error('Error formatting CSS:', error);
124
+ } catch {
125
+ // 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
127
  return node.source;
128
128
  }
129
129
  };
130
130
  }
131
131
 
132
+ // Raw-text `<script>` bodies: the parser mirrors the element's `content` as
133
+ // a single JSXText child. Format it with Prettier's TypeScript parser (a
134
+ // superset of JS, so plain bodies format identically) the same way <style>
135
+ // bodies are formatted as CSS above.
136
+ if (node.type === 'JSXText') {
137
+ const parent = /** @type {AST.TSRXJSXElement | null} */ (path.getParentNode());
138
+ if (isRawScriptElement(parent)) {
139
+ return async (textToDoc) => {
140
+ try {
141
+ const body = await textToDoc(node.value, {
142
+ parser: 'typescript',
143
+ });
144
+ // Drop the program's trailing hardline; printElement places the
145
+ // closing tag on its own line already.
146
+ return stripTrailingHardline(body);
147
+ } catch {
148
+ // A body that doesn't parse (e.g. mid-edit code) is an expected
149
+ // state, not an error: keep it verbatim and stay quiet.
150
+ return replaceEndOfLine(node.value);
151
+ }
152
+ };
153
+ }
154
+ }
155
+
132
156
  return null;
133
157
  },
134
158
  /**
@@ -161,6 +185,23 @@ export const printers = {
161
185
  },
162
186
  };
163
187
 
188
+ /**
189
+ * Raw-text `<script>` element: the parser stores the verbatim JS/TS body on
190
+ * `content` and mirrors it as a single JSXText child. Checking the tag name
191
+ * alongside `content` matches the other raw-aware consumers (the Ripple
192
+ * transforms, the compiler's script regions).
193
+ * @param {AST.TSRXJSXElement | AST.JSXStyleElement | null | undefined} node
194
+ * @returns {boolean}
195
+ */
196
+ function isRawScriptElement(node) {
197
+ return (
198
+ node?.type === 'JSXElement' &&
199
+ node.openingElement?.name?.type === 'JSXIdentifier' &&
200
+ node.openingElement.name.name === 'script' &&
201
+ typeof node.content === 'string'
202
+ );
203
+ }
204
+
164
205
  /**
165
206
  * Format a string literal according to Prettier options
166
207
  * @param {string | number | bigint | boolean | RegExp | null | undefined} value - value to format
@@ -362,6 +403,16 @@ function binaryExpressionNeedsParens(node, parent) {
362
403
  if (nodePrecedence === parentPrecedence && node.operator !== parent.operator) {
363
404
  return true;
364
405
  }
406
+ if (nodePrecedence === parentPrecedence) {
407
+ // Same precedence, same operator: dropping the parens regroups a
408
+ // left-associative chain (`a - (b - c)` !== `a - b - c`; even `+` is
409
+ // non-associative once strings are involved), so the RIGHT operand
410
+ // keeps its parens. `**` is right-associative — there it's the LEFT
411
+ // operand that must keep them.
412
+ if (parent.operator === '**' ? parent.left === node : parent.right === node) {
413
+ return true;
414
+ }
415
+ }
365
416
  }
366
417
 
367
418
  return false;
@@ -5861,6 +5912,24 @@ function printJSXElement(node, path, options, print) {
5861
5912
  { shouldBreak: shouldForceBreak },
5862
5913
  );
5863
5914
 
5915
+ // Raw-text `<script>` element: the body lives on `node.content`, mirrored as a
5916
+ // single JSXText child (see the parser's `#parseScriptElement`). Print that
5917
+ // child — embed() formats it as TypeScript — in a block layout, bypassing the
5918
+ // generic children path so the body is never whitespace-merged as markup text.
5919
+ if (isRawScriptElement(node)) {
5920
+ if (!hasChildren) {
5921
+ return [openingTag, '</', tagName, '>'];
5922
+ }
5923
+ return group([
5924
+ openingTag,
5925
+ indent([hardline, path.call(print, 'children', 0)]),
5926
+ hardline,
5927
+ '</',
5928
+ tagName,
5929
+ '>',
5930
+ ]);
5931
+ }
5932
+
5864
5933
  // Comments before `</tag>` and the comments of a comment-only element.
5865
5934
  const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
5866
5935
  /** @type {AST.TSRXJSXElement} */ (node),
package/src/index.test.js CHANGED
@@ -2285,6 +2285,42 @@ const program =
2285
2285
  expect(result).toBeWithNewline(expected);
2286
2286
  });
2287
2287
 
2288
+ it('should keep parens around the right operand of a same-operator subtraction', async () => {
2289
+ const expected = `const d = a - (b - c);`;
2290
+
2291
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2292
+ expect(result).toBeWithNewline(expected);
2293
+ });
2294
+
2295
+ it('should keep parens around the right operand of a same-operator division', async () => {
2296
+ const expected = `const d = a / (b / c);`;
2297
+
2298
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2299
+ expect(result).toBeWithNewline(expected);
2300
+ });
2301
+
2302
+ it('should keep parens around a right-side addition under string concatenation', async () => {
2303
+ const expected = `const s = 'x' + (n + 1);`;
2304
+
2305
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2306
+ expect(result).toBeWithNewline(expected);
2307
+ });
2308
+
2309
+ it('should drop redundant parens around the left operand of a same-operator addition', async () => {
2310
+ const input = `const s = (a + b) + c;`;
2311
+ const expected = `const s = a + b + c;`;
2312
+
2313
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
2314
+ expect(result).toBeWithNewline(expected);
2315
+ });
2316
+
2317
+ it('should keep parens around the left operand of exponentiation', async () => {
2318
+ const expected = `const p = (a ** b) ** c;`;
2319
+
2320
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
2321
+ expect(result).toBeWithNewline(expected);
2322
+ });
2323
+
2288
2324
  it('should have parents around low-precedence logical expression', async () => {
2289
2325
  const input = `files = [...files ?? [], ...dt.files];
2290
2326
  files = [...(files ?? []), ...dt.files];`;
@@ -5583,13 +5619,49 @@ render(App);`;
5583
5619
  expect(result).toBeWithNewline(expected);
5584
5620
  });
5585
5621
 
5586
- it('should preserve <script> tags', async () => {
5587
- const expected = `<script>const i = 2;</script>`;
5622
+ it('should format <script> bodies as JS in a block layout', async () => {
5623
+ const expected = `<script>
5624
+ const i = 2;
5625
+ </script>`;
5588
5626
 
5589
- const result = await format(expected, { singleQuote: true, printWidth: 100 });
5627
+ const result = await format(`<script>const i = 2;</script>`, {
5628
+ singleQuote: true,
5629
+ printWidth: 100,
5630
+ });
5631
+ expect(result).toBeWithNewline(expected);
5632
+ });
5633
+
5634
+ it('should format a TypeScript <script> body with prettier options applied', async () => {
5635
+ const expected = `<script type="text/typescript">
5636
+ const n: number = 1 < 2 ? 3 : 4;
5637
+ if (n < 2) {
5638
+ go('now');
5639
+ }
5640
+ </script>`;
5641
+
5642
+ const source = `<script type="text/typescript">const n:number=1<2?3:4;
5643
+ if(n<2){go("now")}</script>`;
5644
+
5645
+ const result = await format(source, { singleQuote: true, printWidth: 100 });
5590
5646
  expect(result).toBeWithNewline(expected);
5591
5647
  });
5592
5648
 
5649
+ it('should be idempotent when reformatting a formatted <script> body', async () => {
5650
+ const once = await format(`<script>const i = 2;</script>`, {
5651
+ singleQuote: true,
5652
+ printWidth: 100,
5653
+ });
5654
+ const twice = await format(once, { singleQuote: true, printWidth: 100 });
5655
+ expect(twice).toBe(once);
5656
+ });
5657
+
5658
+ it('should keep an unparseable <script> body verbatim', async () => {
5659
+ const expected = `<script>const broken = ;</script>`;
5660
+
5661
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
5662
+ expect(result).toContain('const broken = ;');
5663
+ });
5664
+
5593
5665
  it('should preserve the blank line between a function and text literal sibling inside element', async () => {
5594
5666
  const expected = `function Something({ children }) {
5595
5667
  const test = 'yo';