Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
run: npm test

test-svelte-5:
name: Test (Svelte 5)
name: Test (latest Svelte 5)
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -55,6 +55,24 @@ jobs:
- name: Run tests with latest Prettier 3.x
run: npm test

test-prettier-earliest:
name: Test (Prettier 3.0.0)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: npm
- name: Install dependencies
run: npm ci
- name: Override Prettier to 3.0.0
run: npm install prettier@3.0.0 --no-save
- name: Run tests with Prettier 3.0.0
run: npm test

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
304 changes: 152 additions & 152 deletions CHANGELOG.md

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Format your Svelte components using Prettier.

## Features

- Format your HTML, CSS, and JavaScript using prettier
- Format Svelte syntax, e.g. each loops, if statements, await blocks, etc.
- Format the JavaScript expressions embedded in the Svelte syntax
- e.g. expressions inside of `{}`, event bindings `on:click=""`, and more
- Format your HTML, CSS, and JavaScript using prettier
- Format Svelte syntax, e.g. each loops, if statements, await blocks, etc.
- Format the JavaScript expressions embedded in the Svelte syntax
- e.g. expressions inside of `{}`, event bindings `on:click=""`, and more

## VS Code Extension

Expand All @@ -21,16 +21,16 @@ The extension lets you define options through extension-specific configuration.

Installing the plugin as a package allows:

- customizing the formatting behavior
- using the command line to format
- using a different IDE
- using the official VS Code [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extension to format Svelte files
- customizing the formatting behavior
- using the command line to format
- using a different IDE
- using the official VS Code [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extension to format Svelte files

### Compatibility

- `prettier-plugin-svelte@4` only works with `prettier@3`
- `prettier-plugin-svelte@3` only works with `prettier@3`
- `prettier-plugin-svelte@2` only works with `prettier@2`
- `prettier-plugin-svelte@4` only works with `prettier@3`
- `prettier-plugin-svelte@3` only works with `prettier@3`
- `prettier-plugin-svelte@2` only works with `prettier@2`

### Setup

Expand All @@ -48,7 +48,7 @@ Then create a `.prettierrc` [configuration file](https://prettier.io/docs/en/con
// ..
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."], // should be removed in v3
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
}
```

Expand All @@ -69,8 +69,8 @@ As part of your scripts in `package.json`:
{
// ..
"scripts": {
"format": "prettier --write ."
}
"format": "prettier --write .",
},
}
```

Expand Down Expand Up @@ -175,17 +175,17 @@ Whether or not to indent the code inside `<script>` and `<style>` tags in Svelte

## Usage with Tailwind Prettier Plugin

- VS Code Extension: Use it as the default formatter for Svelte files
- Prettier Plugin: Load the Tailwind plugin in the end - [Tailwind docs](https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins)
- VS Code Extension: Use it as the default formatter for Svelte files
- Prettier Plugin: Load the Tailwind plugin in the end - [Tailwind docs](https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins)

```jsonc
// .prettierrc
{
// ..
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss" // MUST come last
]
"prettier-plugin-tailwindcss", // MUST come last
],
}
```

Expand All @@ -195,7 +195,7 @@ Since we are using configuration overrides to handle svelte files, you might als
// settings.json
{
// ..
"prettier.documentSelectors": ["**/*.svelte"]
"prettier.documentSelectors": ["**/*.svelte"],
}
```

Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@rollup/plugin-node-resolve": "11.0.1",
"@types/node": "^14.0.0",
"ava": "3.15.0",
"prettier": "^3.0.0",
"prettier": "^3.8.0",
"rollup": "2.36.0",
"rollup-plugin-typescript": "1.0.1",
"svelte": "^5.53.0",
Expand Down
118 changes: 67 additions & 51 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {
ScriptNode,
StyleNode,
} from './print/nodes';
import { extractAttributes } from './lib/extractAttributes';
import { base64ToString } from './base64-string';
import { AST } from 'svelte/compiler';

const {
builders: { group, hardline, softline, indent, dedent, literalline },
Expand All @@ -39,7 +39,7 @@ const {

const leaveAlone = new Set([
'Script',
'Style',
'StyleSheet',
'Identifier',
'MemberExpression',
'CallExpression',
Expand All @@ -59,7 +59,7 @@ export function getVisitorKeys(node: any, nonTraversableKeys: Set<string>): stri
// - if embed returns a function, it will be called after the traversal in a second pass, in the same order (deepest first)
// For performance reasons we try to only return functions when we're sure we need to transform something.
export function embed(path: AstPath, _options: Options) {
const node: Node = path.getNode();
const node = path.getNode() as any;
const options = _options as ParserOptions;
if (!options.locStart || !options.locEnd || !options.originalText) {
throw new Error('Missing required options');
Expand All @@ -68,31 +68,20 @@ export function embed(path: AstPath, _options: Options) {
if (isASTNode(node)) {
assignCommentsToNodes(node);
attachAttributeComments(node, options.originalText);
if (node.module) {
node.module.type = 'Script';
node.module.attributes = extractAttributes(getText(node.module, options));
}
if (node.instance) {
node.instance.type = 'Script';
node.instance.attributes = extractAttributes(getText(node.instance, options));
}
if (node.css) {
node.css.type = 'Style';
node.css.content.type = 'StyleProgram';
}
return null;
}

// embed does depth first traversal with deepest node called first, therefore we need to
// check the parent to see if we are inside an expression that should be embedded.
const parent: Node = path.getParentNode();
const parent = path.getParentNode() as any;
const printJsExpression = () =>
(parent as any).expression ? printJS(parent, 'expression', {}) : undefined;
const printSvelteBlockJS = (name: string) => printJS(parent, name, { forceSingleLine: true });

switch (parent.type) {
case 'IfBlock':
case 'ElseBlock':
printSvelteBlockJS('test');
break;
case 'AwaitBlock':
case 'KeyBlock':
printSvelteBlockJS('expression');
Expand All @@ -117,27 +106,29 @@ export function embed(path: AstPath, _options: Options) {
node.asFunction = true;
}
break;
case 'Element':
case 'RegularElement':
case 'SvelteElement':
printJS(parent, 'tag', {});
break;
case 'MustacheTag':
case 'ExpressionTag':
printJS(parent, 'expression', {
forceSingleQuote: isInsideQuotedAttribute(path, options),
});
break;
case 'RawMustacheTag':
case 'HtmlTag':
printJS(parent, 'expression', {});
break;
case 'Spread':
case 'SpreadAttribute':
printJS(parent, 'expression', {});
break;
case 'AttachTag':
printJS(parent, 'expression', {});
break;
case 'ConstTag':
(parent as any).expression = (parent as AST.ConstTag).declaration.declarations[0];
printJS(parent, 'expression', { removeParentheses: true });
break;
case 'Binding':
case 'BindDirective':
printJS(parent, 'expression', {
removeParentheses: parent.expression.type === 'SequenceExpression',
surroundWithSoftline: true,
Expand All @@ -160,14 +151,14 @@ export function embed(path: AstPath, _options: Options) {
printJS(parent, 'expression', {});
}
break;
case 'EventHandler':
case 'Binding':
case 'Class':
case 'Let':
case 'Transition':
case 'Action':
case 'Animation':
case 'InlineComponent':
case 'OnDirective':
case 'BindDirective':
case 'ClassDirective':
case 'LetDirective':
case 'TransitionDirective':
case 'UseDirective':
case 'AnimateDirective':
case 'SvelteComponent':
printJsExpression();
break;
}
Expand Down Expand Up @@ -256,9 +247,9 @@ export function embed(path: AstPath, _options: Options) {
switch (node.type) {
case 'Script':
return embedScript(true);
case 'Style':
case 'StyleSheet':
return embedStyle(true);
case 'Element': {
case 'RegularElement': {
if (node.name === 'script') {
return embedScript(false);
} else if (node.name === 'style') {
Expand Down Expand Up @@ -359,12 +350,12 @@ async function embedTag(
isTopLevel: boolean,
options: ParserOptions,
) {
const node: ScriptNode | StyleNode | ElementNode = path.getNode();
const node = path.getNode() as ScriptNode | StyleNode | ElementNode;
const content =
tag === 'template' ? printRaw(node as ElementNode, text) : getSnippedContent(node);
const previousComments =
node.type === 'Script' || node.type === 'Style'
? node.comments
node.type === 'Script' || node.type === 'StyleSheet'
? (node.comments ?? [])
: [getLeadingComment(path)]
.filter(Boolean)
.map((comment) => ({ comment: comment as CommentNode, emptyLineAfter: false }));
Expand All @@ -374,14 +365,20 @@ async function embedTag(
!isIgnoreDirective(previousComments[previousComments.length - 1]?.comment) &&
(tag !== 'template' ||
options.plugins.some(
(plugin) => typeof plugin !== 'string' && plugin.parsers && plugin.parsers.pug,
(plugin) =>
typeof plugin !== 'string' &&
!(plugin instanceof URL) &&
// @ts-expect-error Prettier's type definitions don't include name anymore for some reason
plugin.name === 'prettier-plugin-svelte' &&
plugin.parsers &&
plugin.parsers.pug,
));
const body: Doc = canFormat
? content.trim() !== ''
? await formatBodyContent(content)
: content === ''
? ''
: hardline
? ''
: hardline
: preformattedBody(content);

const openingTag = group([
Expand Down Expand Up @@ -453,10 +450,14 @@ function attachAttributeComments(ast: ASTNode, original_text: string): void {
commentsByStart.set(c.start, c);
}

walkAndAttach(ast.html, commentsByStart, original_text);
walkAndAttach(ast.fragment as any, commentsByStart, original_text);
}

function walkAndAttach(node: Node, commentsByStart: Map<number, any>, original_text: string): void {
function walkAndAttach(
node: Node | AST.Fragment,
commentsByStart: Map<number, any>,
original_text: string,
): void {
if (!node || typeof node !== 'object') return;

if ('attributes' in node && Array.isArray(node.attributes) && node.attributes.length > 0) {
Expand Down Expand Up @@ -489,17 +490,32 @@ function walkAndAttach(node: Node, commentsByStart: Map<number, any>, original_t
}

// Recurse into children and block branches
for (const child of getChildren(node)) {
walkAndAttach(child, commentsByStart, original_text);
}

if ((node.type === 'IfBlock' || node.type === 'EachBlock') && node.else) {
walkAndAttach(node.else, commentsByStart, original_text);
}
if (node.type === 'AwaitBlock') {
if (node.pending) walkAndAttach(node.pending, commentsByStart, original_text);
if (node.then) walkAndAttach(node.then, commentsByStart, original_text);
if (node.catch) walkAndAttach(node.catch, commentsByStart, original_text);
// Some of these have multiple fragment nodes so we need to recurse them separately
if (node.type === 'IfBlock') {
if (node.consequent) {
walkAndAttach(node.consequent!, commentsByStart, original_text);
}
if (node.alternate) {
walkAndAttach(node.alternate!, commentsByStart, original_text);
}
} else if (node.type === 'EachBlock') {
if (node.body) {
walkAndAttach(node.body!, commentsByStart, original_text);
}
if (node.fallback) {
walkAndAttach(node.fallback!, commentsByStart, original_text);
}
} else if (node.type === 'AwaitBlock') {
if ((node as AST.AwaitBlock).pending)
walkAndAttach((node as AST.AwaitBlock).pending!, commentsByStart, original_text);
if ((node as AST.AwaitBlock).then)
walkAndAttach((node as AST.AwaitBlock).then!, commentsByStart, original_text);
if ((node as AST.AwaitBlock).catch)
walkAndAttach((node as AST.AwaitBlock).catch!, commentsByStart, original_text);
} else {
for (const child of getChildren(node)) {
walkAndAttach(child, commentsByStart, original_text);
}
}
}

Expand Down
Loading
Loading