diff --git a/src/embed.ts b/src/embed.ts index 195bc62..2afe500 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -3,7 +3,7 @@ import { getText } from './lib/getText'; import { snippedTagContentAttribute } from './lib/snipTagContent'; import { isBracketSameLine, ParserOptions } from './options'; import { PrintFn } from './print'; -import { isLine, removeParentheses, trimRight } from './print/doc-helpers'; +import { isLine, removeParentheses, removeLeadingSemicolon, trimRight } from './print/doc-helpers'; import { isASTNode, printWithPrependedAttributeLine } from './print/helpers'; import { assignCommentsToNodes, @@ -157,13 +157,16 @@ export function embed(path: AstPath, _options: Options) { ): Promise => { try { const embeddedOptions = { - // Prettier only allows string references as parsers from v3 onwards, - // so we need to have another public parser and defer to that - parser: options._svelte_ts - ? 'svelteTSExpressionParser' - : 'svelteExpressionParser', + // Use babel-ts/babel directly for Prettier 3.7.0+ compatibility. + // This fixes generic type parameters being stripped. The custom parsers + // (svelteTSExpressionParser/svelteExpressionParser) manipulate the AST + // in a way that's incompatible with Prettier 3.7.0+. + // Note: Using babel-ts directly may affect parentheses formatting compared + // to previous versions, as Prettier removes "unnecessary" parentheses. + parser: options._svelte_ts ? 'babel-ts' : 'babel', singleQuote: node.forceSingleQuote ? true : options.singleQuote, - _svelte_asFunction: node.asFunction, + // Don't add semicolons to expressions + semi: false, }; // If we have snipped content, it was done wrongly and we need to unsnip it. @@ -176,7 +179,9 @@ export function embed(path: AstPath, _options: Options) { if (node.forceSingleLine) { docs = removeLines(docs); } - if (node.removeParentheses) { + // For non-function expressions, always remove the wrapping parentheses + // and protective semicolons added by forceIntoExpression and Prettier + if (!node.asFunction) { docs = removeParentheses(docs); } if (node.asFunction) { diff --git a/src/index.ts b/src/index.ts index 59cace7..507c171 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,8 @@ export const parsers: Record = { parse: (text: string, options: any) => { const ast = babelParser.parse(text, options); + // Extract just the expression from the wrapper statement for non-functions + // This preserves the expression structure including necessary parentheses let program = ast.program.body[0]; if (!options._svelte_asFunction) { program = program.expression; @@ -80,6 +82,8 @@ export const parsers: Record = { parse: (text: string, options: any) => { const ast = typescriptParser.parse(text, options); + // Extract just the expression from the wrapper statement for non-functions + // This preserves the expression structure including necessary parentheses let program = ast.program.body[0]; if (!options._svelte_asFunction) { program = program.expression; diff --git a/src/print/doc-helpers.ts b/src/print/doc-helpers.ts index a951f27..57db9d8 100644 --- a/src/print/doc-helpers.ts +++ b/src/print/doc-helpers.ts @@ -153,9 +153,95 @@ function getParts(doc: Doc): Doc[] | undefined { } } +/** + * Remove leading semicolons added by Prettier for ASI protection + */ +export function removeLeadingSemicolon(doc: Doc): Doc { + // Handle string docs + if (typeof doc === 'string') { + let str = doc; + // Remove leading semicolon (ASI protection added by Prettier) + while (str.startsWith(';')) { + str = str.slice(1); + } + return str; + } + + // Handle array docs + if (Array.isArray(doc)) { + const result = [...doc]; + // Remove leading semicolons from the first element + while (result.length > 0 && typeof result[0] === 'string' && result[0].startsWith(';')) { + result[0] = result[0].slice(1); + if (result[0] === '') { + result.shift(); + } + } + return result; + } + + // For other doc types, return as-is + return doc; +} + /** * `(foo = bar)` => `foo = bar` + * Also removes leading semicolons added by Prettier for ASI protection */ export function removeParentheses(doc: Doc): Doc { - return trim([doc], (_doc: Doc) => _doc === '(' || _doc === ')')[0]; + // Handle string docs that have semicolons and/or parentheses + if (typeof doc === 'string') { + let str = doc; + // Remove leading semicolon (ASI protection added by Prettier) + while (str.startsWith(';')) { + str = str.slice(1); + } + // For simple expressions, Prettier keeps wrapping parentheses even with semi:false + // e.g., ('foo') stays as ('foo'). We need to remove these for single expressions. + // We use a very conservative heuristic: only remove if it starts with '(', ends with ')', + // and has no other parentheses anywhere inside. This ensures we only remove wrapping + // from simple literals and avoid touching expressions like function calls or grouping. + // Note: This is intentionally conservative to avoid removing semantically important + // parentheses. It means some unnecessary wrapping may remain, which is acceptable. + if (str.startsWith('(') && str.endsWith(')')) { + const hasNoInnerParens = str.indexOf('(', 1) === -1 && str.indexOf(')', str.length - 2) === str.length - 1; + if (hasNoInnerParens) { + // Only one set of parentheses wrapping the whole thing + str = str.slice(1, -1); + } + } + return str; + } + + // Handle array docs + if (Array.isArray(doc)) { + const result = [...doc]; + // Remove leading semicolons from the first element + while (result.length > 0 && typeof result[0] === 'string' && result[0].startsWith(';')) { + result[0] = result[0].slice(1); + if (result[0] === '') { + result.shift(); + } + } + + // If the result is a single string, handle it recursively + // Only recurse if we actually made a change (removed semicolons) to avoid infinite recursion + if (result.length === 1 && typeof result[0] === 'string') { + const originalFirstElement = Array.isArray(doc) ? doc[0] : doc; + const wasChanged = result[0] !== originalFirstElement; + if (wasChanged) { + return removeParentheses(result[0]); + } + return result[0]; + } + + // For multi-element arrays, just return after removing leading semicolons + // Don't try to remove parentheses as they might be part of the expression structure + // (e.g., function call parentheses, not wrapping from forceIntoExpression) + return result; + } + + // For other doc types (objects/groups), don't modify them + // as they're complex structures that shouldn't have wrapping removed + return doc; } diff --git a/test/formatting/samples/generic-type-parameters/input.html b/test/formatting/samples/generic-type-parameters/input.html new file mode 100644 index 0000000..3914c3e --- /dev/null +++ b/test/formatting/samples/generic-type-parameters/input.html @@ -0,0 +1,6 @@ + + +), baz: 2 })}>Go diff --git a/test/formatting/samples/generic-type-parameters/output.html b/test/formatting/samples/generic-type-parameters/output.html new file mode 100644 index 0000000..1239fa7 --- /dev/null +++ b/test/formatting/samples/generic-type-parameters/output.html @@ -0,0 +1,11 @@ + + +), + baz: 2, + })}>Go