@tsrx/prettier-plugin 0.3.93 → 0.3.94

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.94",
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
@@ -5861,6 +5902,24 @@ function printJSXElement(node, path, options, print) {
5861
5902
  { shouldBreak: shouldForceBreak },
5862
5903
  );
5863
5904
 
5905
+ // Raw-text `<script>` element: the body lives on `node.content`, mirrored as a
5906
+ // single JSXText child (see the parser's `#parseScriptElement`). Print that
5907
+ // child — embed() formats it as TypeScript — in a block layout, bypassing the
5908
+ // generic children path so the body is never whitespace-merged as markup text.
5909
+ if (isRawScriptElement(node)) {
5910
+ if (!hasChildren) {
5911
+ return [openingTag, '</', tagName, '>'];
5912
+ }
5913
+ return group([
5914
+ openingTag,
5915
+ indent([hardline, path.call(print, 'children', 0)]),
5916
+ hardline,
5917
+ '</',
5918
+ tagName,
5919
+ '>',
5920
+ ]);
5921
+ }
5922
+
5864
5923
  // Comments before `</tag>` and the comments of a comment-only element.
5865
5924
  const { closingCommentDocs, innerCommentDocs } = collectElementBodyCommentDocs(
5866
5925
  /** @type {AST.TSRXJSXElement} */ (node),
package/src/index.test.js CHANGED
@@ -5583,13 +5583,49 @@ render(App);`;
5583
5583
  expect(result).toBeWithNewline(expected);
5584
5584
  });
5585
5585
 
5586
- it('should preserve <script> tags', async () => {
5587
- const expected = `<script>const i = 2;</script>`;
5586
+ it('should format <script> bodies as JS in a block layout', async () => {
5587
+ const expected = `<script>
5588
+ const i = 2;
5589
+ </script>`;
5588
5590
 
5589
- const result = await format(expected, { singleQuote: true, printWidth: 100 });
5591
+ const result = await format(`<script>const i = 2;</script>`, {
5592
+ singleQuote: true,
5593
+ printWidth: 100,
5594
+ });
5595
+ expect(result).toBeWithNewline(expected);
5596
+ });
5597
+
5598
+ it('should format a TypeScript <script> body with prettier options applied', async () => {
5599
+ const expected = `<script type="text/typescript">
5600
+ const n: number = 1 < 2 ? 3 : 4;
5601
+ if (n < 2) {
5602
+ go('now');
5603
+ }
5604
+ </script>`;
5605
+
5606
+ const source = `<script type="text/typescript">const n:number=1<2?3:4;
5607
+ if(n<2){go("now")}</script>`;
5608
+
5609
+ const result = await format(source, { singleQuote: true, printWidth: 100 });
5590
5610
  expect(result).toBeWithNewline(expected);
5591
5611
  });
5592
5612
 
5613
+ it('should be idempotent when reformatting a formatted <script> body', async () => {
5614
+ const once = await format(`<script>const i = 2;</script>`, {
5615
+ singleQuote: true,
5616
+ printWidth: 100,
5617
+ });
5618
+ const twice = await format(once, { singleQuote: true, printWidth: 100 });
5619
+ expect(twice).toBe(once);
5620
+ });
5621
+
5622
+ it('should keep an unparseable <script> body verbatim', async () => {
5623
+ const expected = `<script>const broken = ;</script>`;
5624
+
5625
+ const result = await format(expected, { singleQuote: true, printWidth: 100 });
5626
+ expect(result).toContain('const broken = ;');
5627
+ });
5628
+
5593
5629
  it('should preserve the blank line between a function and text literal sibling inside element', async () => {
5594
5630
  const expected = `function Something({ children }) {
5595
5631
  const test = 'yo';