diff --git a/CHANGELOG.md b/CHANGELOG.md index 4132e49..c852b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # prettier-plugin-svelte changelog -## 3.2.7 (unreleased) +## 3.3.3 + +- (fix) Svelte 5: ensure bind get/set is broken up correctly when too long + +## 3.3.2 + +- (fix) Svelte 5: handle type annotations on Svelte control flow blocks +- (fix) preserve `style`/`script` tags at the end of the file when using `svelteSortOrder: "none"` + +## 3.3.1 + +- (feat) Svelte 5: support upcoming `bind:value={get, set}` + +## 3.3.0 + +- (feat) Svelte 5: support upcoming `` +- (feat) Svelte 5: support upcoming `` +- (feat) Svelte 5: support upcoming `#each` without `as` + +## 3.2.8 + +- (chore) provide IDE tooling a way to pass Svelte compiler path + +## 3.2.7 - (fix) force quote style inside style directives - (fix) preserve commas in array expressions diff --git a/index.d.ts b/index.d.ts index 9c8975a..1adfd0e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,7 @@ export interface PluginConfig { svelteBracketNewLine?: boolean; svelteAllowShorthand?: boolean; svelteIndentScriptAndStyle?: boolean; + svelte5CompilerPath?: string; } export type PrettierConfig = PluginConfig & Config; diff --git a/package-lock.json b/package-lock.json index 2b7590c..adf3ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prettier-plugin-svelte", - "version": "3.2.6", + "version": "3.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prettier-plugin-svelte", - "version": "3.2.6", + "version": "3.3.3", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/package.json b/package.json index a9b482c..f537bf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prettier-plugin-svelte", - "version": "3.2.6", + "version": "3.3.3", "description": "Svelte plugin for prettier", "main": "plugin.js", "files": [ diff --git a/src/embed.ts b/src/embed.ts index c0018fe..8301b16 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -78,15 +78,11 @@ export function embed(path: FastPath, _options: Options) { const parent: Node = path.getParentNode(); const printJsExpression = () => (parent as any).expression - ? printJS( - parent, - (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, - false, - false, - 'expression', - ) + ? printJS(parent, 'expression', { + forceSingleQuote: (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, + }) : undefined; - const printSvelteBlockJS = (name: string) => printJS(parent, false, true, false, name); + const printSvelteBlockJS = (name: string) => printJS(parent, name, { forceSingleLine: true }); switch (parent.type) { case 'IfBlock': @@ -116,25 +112,26 @@ export function embed(path: FastPath, _options: Options) { } break; case 'Element': - printJS( - parent, - (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, - false, - false, - 'tag', - ); + printJS(parent, 'tag', { + forceSingleQuote: (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, + }); break; case 'MustacheTag': - printJS(parent, isInsideQuotedAttribute(path, options), false, false, 'expression'); + printJS(parent, 'expression', { + forceSingleQuote: isInsideQuotedAttribute(path, options), + }); break; case 'RawMustacheTag': - printJS(parent, false, false, false, 'expression'); + printJS(parent, 'expression', {}); break; case 'Spread': - printJS(parent, false, false, false, 'expression'); + printJS(parent, 'expression', {}); break; case 'ConstTag': - printJS(parent, false, false, true, 'expression'); + printJS(parent, 'expression', { removeParentheses: true }); + break; + case 'Binding': + printJS(parent, 'expression', { removeParentheses: true, surroundWithSoftline: true }); break; case 'RenderTag': if (node === parent.expression) { @@ -150,7 +147,7 @@ export function embed(path: FastPath, _options: Options) { parent.argument = null; parent.arguments = null; } - printJS(parent, false, false, false, 'expression'); + printJS(parent, 'expression', {}); } break; case 'EventHandler': @@ -201,6 +198,9 @@ export function embed(path: FastPath, _options: Options) { throw new Error('Prettier AST changed, asFunction logic needs to change'); } } + if (node.surroundWithSoftline) { + docs = group(indent([softline, group(docs), dedent(softline)])); + } return docs; } catch (e) { return getText(node, options, true); @@ -409,17 +409,21 @@ async function embedTag( function printJS( node: any, - forceSingleQuote: boolean, - forceSingleLine: boolean, - removeParentheses: boolean, name: string, + options: { + forceSingleQuote?: boolean; + forceSingleLine?: boolean; + removeParentheses?: boolean; + surroundWithSoftline?: boolean; + }, ) { const part = node[name] as BaseNode | undefined; if (!part || typeof part !== 'object') { return; } part.isJS = true; - part.forceSingleQuote = forceSingleQuote; - part.forceSingleLine = forceSingleLine; - part.removeParentheses = removeParentheses; + part.forceSingleQuote = options.forceSingleQuote; + part.forceSingleLine = options.forceSingleLine; + part.removeParentheses = options.removeParentheses; + part.surroundWithSoftline = options.surroundWithSoftline; } diff --git a/src/index.ts b/src/index.ts index e661d5f..dea7ba6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,9 +31,22 @@ export const languages: Partial[] = [ export const parsers: Record = { svelte: { hasPragma, - parse: (text) => { + parse: async (text, options: ParserOptions) => { try { - return { ...parse(text), __isRoot: true }; + let _parse = parse; + if (options.svelte5CompilerPath) { + try { + _parse = (await import(options.svelte5CompilerPath)).parse; + } catch (e) { + console.warn( + `Failed to load Svelte 5 compiler from ${options.svelte5CompilerPath}`, + ); + console.warn(e); + options.svelte5CompilerPath = undefined; + } + } + + return { ..._parse(text), __isRoot: true }; } catch (err: any) { if (err.start != null && err.end != null) { // Prettier expects error objects to have loc.start and loc.end fields. @@ -57,8 +70,9 @@ export const parsers: Record = { // Therefore we do it ourselves here. options.originalText = text; // Only Svelte 5 can have TS in the template - options._svelte_ts = isSvelte5Plus && result.isTypescript; - options._svelte_is5Plus = isSvelte5Plus; + const is = !!options.svelte5CompilerPath || isSvelte5Plus; + options._svelte_ts = is && result.isTypescript; + options._svelte_is5Plus = is; return text; }, locStart, diff --git a/src/options.ts b/src/options.ts index 18d65fd..f5c504b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -19,6 +19,12 @@ function makeChoice(choice: string) { } export const options: Record = { + svelte5CompilerPath: { + category: 'Svelte', + type: 'string', + default: '', + description: 'Only set this when using Svelte 5! Path to the Svelte 5 compiler', + }, svelteSortOrder: { category: 'Svelte', type: 'choice', diff --git a/src/print/helpers.ts b/src/print/helpers.ts index a33130c..d8864a5 100644 --- a/src/print/helpers.ts +++ b/src/print/helpers.ts @@ -16,6 +16,8 @@ import { SlotNode, SlotTemplateNode, StyleNode, + SvelteBoundary, + SvelteHTML, TitleNode, WindowNode, } from './nodes'; @@ -81,6 +83,8 @@ export function getAttributeLine( | BodyNode | DocumentNode | OptionsNode + | SvelteHTML + | SvelteBoundary | SlotTemplateNode, options: ParserOptions, ) { @@ -111,6 +115,8 @@ export function printWithPrependedAttributeLine( | BodyNode | DocumentNode | OptionsNode + | SvelteHTML + | SvelteBoundary | SlotTemplateNode, options: ParserOptions, print: PrintFn, diff --git a/src/print/index.ts b/src/print/index.ts index c4b81a3..01cbce0 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -202,6 +202,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D case 'SlotTemplate': case 'Window': case 'Head': + // Svelte 5 only + case 'SvelteBoundary': case 'Title': { const isSupportedLanguage = !( node.name === 'template' && !isNodeSupportedLanguage(node) @@ -217,6 +219,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D node.type === 'InlineComponent' || node.type === 'Slot' || node.type === 'SlotTemplate' || + node.type === 'SvelteBoundary' || node.type === 'Title') && didSelfClose) || node.type === 'Window' || @@ -400,21 +403,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D // else fall through to Body case 'Body': case 'Document': - return group([ - '<', - node.name, - indent( - group([ - ...path.map( - printWithPrependedAttributeLine(node, options, print), - 'attributes', - ), - bracketSameLine ? '' : dedent(line), - ]), - ), - ...[bracketSameLine ? ' ' : '', '/>'], - ]); - case 'Document': + // Svelte 5 only + case 'SvelteHTML': return group([ '<', node.name, @@ -509,12 +499,11 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return ['{:else}', printSvelteBlockChildren(path, print, options)]; } case 'EachBlock': { - const def: Doc[] = [ - '{#each ', - printJS(path, print, 'expression'), - ' as', - expandNode(node.context, options.originalText), - ]; + const def: Doc[] = ['{#each ', printJS(path, print, 'expression')]; + + if (node.context) { + def.push(' as', expandNode(node.context, options.originalText)); + } if (node.index) { def.push(', ', node.index); @@ -744,15 +733,19 @@ function printTopLevelParts( ): Doc { if (options.svelteSortOrder === 'none') { const topLevelPartsByEnd: Record = {}; + const topLevelPartsByStart: Record = {}; if (n.module) { topLevelPartsByEnd[n.module.end] = n.module; + topLevelPartsByStart[n.module.start] = n.module; } if (n.instance) { topLevelPartsByEnd[n.instance.end] = n.instance; + topLevelPartsByStart[n.instance.start] = n.instance; } if (n.css) { topLevelPartsByEnd[n.css.end] = n.css; + topLevelPartsByStart[n.css.start] = n.css; } const children = getChildren(n.html); @@ -761,6 +754,8 @@ function printTopLevelParts( if (topLevelPartsByEnd[node.start]) { children.splice(i, 0, topLevelPartsByEnd[node.start]); delete topLevelPartsByEnd[node.start]; + } else if (i === children.length - 1 && topLevelPartsByStart[node.end]) { + children.push(topLevelPartsByStart[node.end]); } } @@ -1188,7 +1183,12 @@ function printJS(path: FastPath, print: PrintFn, name: string) { function expandNode(node: any, original: string): string { let str = _expandNode(node); if (node?.typeAnnotation) { - str += ': ' + original.slice(node.typeAnnotation.start, node.typeAnnotation.end); + str += + ': ' + + original.slice( + node.typeAnnotation.typeAnnotation.start, + node.typeAnnotation.typeAnnotation.end, + ); } return str; } diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts index 7c0e92f..99668fc 100644 --- a/src/print/node-helpers.ts +++ b/src/print/node-helpers.ts @@ -23,6 +23,7 @@ import { StyleDirectiveNode, ASTNode, CommentInfo, + SvelteBoundary, } from './nodes'; import { blockElements, TagName } from '../lib/elements'; import { FastPath } from 'prettier'; @@ -31,7 +32,11 @@ import { ParserOptions, isBracketSameLine } from '../options'; const unsupportedLanguages = ['coffee', 'coffeescript', 'styl', 'stylus', 'sass']; -export function isInlineElement(path: FastPath, options: ParserOptions, node: Node) { +export function isInlineElement( + path: FastPath, + options: ParserOptions, + node: Node, +): node is ElementNode { return ( node && node.type === 'Element' && !isBlockElement(node, options) && !isPreTagContent(path) ); @@ -180,6 +185,7 @@ export function printRaw( | WindowNode | HeadNode | TitleNode + | SvelteBoundary | SlotTemplateNode, originalText: string, stripLeadingAndTrailingNewline: boolean = false, @@ -400,6 +406,10 @@ export function shouldHugStart( return true; } + if (node.type === 'SvelteBoundary') { + return false; + } + if (isBlockElement(node, options)) { return false; } @@ -434,6 +444,10 @@ export function shouldHugEnd( return true; } + if (node.type === 'SvelteBoundary') { + return false; + } + if (isBlockElement(node, options)) { return false; } diff --git a/src/print/nodes.ts b/src/print/nodes.ts index 30b7094..6988c04 100644 --- a/src/print/nodes.ts +++ b/src/print/nodes.ts @@ -11,6 +11,8 @@ export interface BaseNode { forceSingleLine?: boolean; /** Whether or not to remove outer `()` when printing as JS */ removeParentheses?: boolean; + /** Whether or not to surround the result with a group and softline so that an exceeding print with keeps the output on the same line, if possible */ + surroundWithSoftline?: boolean; } export interface FragmentNode extends BaseNode { @@ -268,6 +270,19 @@ export interface BodyNode extends BaseNode { attributes: Node[]; } +export interface SvelteHTML extends BaseNode { + type: 'SvelteHTML'; + name: string; + attributes: Node[]; +} + +export interface SvelteBoundary extends BaseNode { + type: 'SvelteBoundary'; + name: string; + attributes: Node[]; + children: Node[]; +} + export interface DocumentNode extends BaseNode { type: 'Document'; name: string; @@ -357,6 +372,8 @@ export type Node = | OptionsNode | SlotTemplateNode | ConstTagNode + | SvelteBoundary + | SvelteHTML | RenderTag | SnippetBlock; diff --git a/test/printer/samples/binding-get-set.html.skip b/test/printer/samples/binding-get-set.html.skip new file mode 100644 index 0000000..10bd690 --- /dev/null +++ b/test/printer/samples/binding-get-set.html.skip @@ -0,0 +1,20 @@ + value, (v) => (value = v)} /> + + + + + + + + getter_setter_each_need_own_line, + (v) => (getter_setter_each_need_own_line = v) + } +/> diff --git a/test/printer/samples/each-block-without-as.html.skip b/test/printer/samples/each-block-without-as.html.skip new file mode 100644 index 0000000..28ca782 --- /dev/null +++ b/test/printer/samples/each-block-without-as.html.skip @@ -0,0 +1,7 @@ +{#each [1, 2, 3]} +

hi

+{/each} + +{#each [1, 2, 3], i} +

{i}

+{/each} diff --git a/test/printer/samples/sort-order-none3.html b/test/printer/samples/sort-order-none3.html new file mode 100644 index 0000000..8d4edeb --- /dev/null +++ b/test/printer/samples/sort-order-none3.html @@ -0,0 +1,7 @@ +
this template ends with a special tag
+ + diff --git a/test/printer/samples/sort-order-none3.options.json b/test/printer/samples/sort-order-none3.options.json new file mode 100644 index 0000000..fd6e47a --- /dev/null +++ b/test/printer/samples/sort-order-none3.options.json @@ -0,0 +1,3 @@ +{ + "svelteSortOrder": "none" +} diff --git a/test/printer/samples/svelte-boundary.html.skip b/test/printer/samples/svelte-boundary.html.skip new file mode 100644 index 0000000..8ed5e2a --- /dev/null +++ b/test/printer/samples/svelte-boundary.html.skip @@ -0,0 +1,6 @@ + + + {#snippet failed(e)} + {e} + {/snippet} + diff --git a/test/printer/samples/svelte-html-element.html.skip b/test/printer/samples/svelte-html-element.html.skip new file mode 100644 index 0000000..63a8592 --- /dev/null +++ b/test/printer/samples/svelte-html-element.html.skip @@ -0,0 +1 @@ + diff --git a/test/printer/samples/typescript-template.html.skip b/test/printer/samples/typescript-template.html.skip index ce5c104..4bc1be1 100644 --- a/test/printer/samples/typescript-template.html.skip +++ b/test/printer/samples/typescript-template.html.skip @@ -7,3 +7,15 @@ > + +{#await promise} +

Waiting

+{:then num: number} +

{num}

+{:catch error: Error} +

Error: {error.message}

+{/await} + +{#each [] as x: number} + {x} +{/each}