Skip to content

Commit 59fc0e9

Browse files
authored
feat: (experimental) partial support for Svelte v5 parser (#421)
1 parent d3047a4 commit 59fc0e9

23 files changed

+1803
-117
lines changed

.changeset/friendly-hats-flow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: (experimental) partial support for Svelte v5 parser

.github/workflows/NodeCI.yml

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ jobs:
3535
run: pnpm install
3636
- name: Test
3737
run: pnpm run test
38+
test-for-svelte-v4:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
- uses: pnpm/action-setup@v2
43+
- name: Use Node.js
44+
uses: actions/setup-node@v4
45+
- name: Install Svelte v4
46+
run: |+
47+
pnpm install -D svelte@4
48+
rm -rf node_modules
49+
- name: Install Packages
50+
run: pnpm install
51+
- name: Test
52+
run: pnpm run test
3853
test-for-svelte-v3:
3954
runs-on: ubuntu-latest
4055
strategy:

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"version:ci": "env-cmd -e version-ci pnpm run build:meta && changeset version"
4646
},
4747
"peerDependencies": {
48-
"svelte": "^3.37.0 || ^4.0.0"
48+
"svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.2"
4949
},
5050
"peerDependenciesMeta": {
5151
"svelte": {
@@ -103,8 +103,8 @@
103103
"prettier-plugin-svelte": "^3.0.0",
104104
"rimraf": "^5.0.1",
105105
"semver": "^7.5.1",
106-
"svelte": "^4.2.0",
107-
"svelte2tsx": "^0.6.20",
106+
"svelte": "^5.0.0-next.2",
107+
"svelte2tsx": "^0.6.25",
108108
"typescript": "~5.1.3",
109109
"typescript-eslint-parser-for-extra-files": "^0.5.0"
110110
},

src/parser/converts/element.ts

+112-68
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { ParseError } from "../..";
5252
/** Convert for Fragment or Element or ... */
5353
export function* convertChildren(
5454
/* eslint-enable complexity -- X */
55-
fragment: { children: SvAST.TemplateNode[] },
55+
fragment: { children?: SvAST.TemplateNode[] },
5656
parent:
5757
| SvelteProgram
5858
| SvelteElement
@@ -76,6 +76,7 @@ export function* convertChildren(
7676
| SvelteKeyBlock
7777
| SvelteHTMLComment
7878
> {
79+
if (!fragment.children) return;
7980
for (const child of fragment.children) {
8081
if (child.type === "Comment") {
8182
yield convertComment(child, parent, ctx);
@@ -199,8 +200,9 @@ function extractLetDirectives(fragment: {
199200

200201
/** Check if children needs a scope. */
201202
function needScopeByChildren(fragment: {
202-
children: SvAST.TemplateNode[];
203+
children?: SvAST.TemplateNode[];
203204
}): boolean {
205+
if (!fragment.children) return false;
204206
for (const child of fragment.children) {
205207
if (child.type === "ConstTag") {
206208
return true;
@@ -437,66 +439,92 @@ function processThisAttribute(
437439
(c) => Boolean(c.trim()),
438440
eqIndex + 1,
439441
);
440-
const quote = ctx.code.startsWith(thisValue, valueStartIndex)
441-
? null
442-
: ctx.code[valueStartIndex];
443-
const literalStartIndex = quote
444-
? valueStartIndex + quote.length
445-
: valueStartIndex;
446-
const literalEndIndex = literalStartIndex + thisValue.length;
447-
const endIndex = quote ? literalEndIndex + quote.length : literalEndIndex;
448-
const thisAttr: SvelteAttribute = {
449-
type: "SvelteAttribute",
450-
key: null as any,
451-
boolean: false,
452-
value: [],
453-
parent: element.startTag,
454-
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
455-
};
456-
thisAttr.key = {
457-
type: "SvelteName",
458-
name: "this",
459-
parent: thisAttr,
460-
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
461-
};
462-
thisAttr.value.push({
463-
type: "SvelteLiteral",
464-
value: thisValue,
465-
parent: thisAttr,
466-
...ctx.getConvertLocation({
467-
start: literalStartIndex,
468-
end: literalEndIndex,
469-
}),
470-
});
471-
// this
472-
ctx.addToken("HTMLIdentifier", {
473-
start: startIndex,
474-
end: startIndex + 4,
475-
});
476-
// =
477-
ctx.addToken("Punctuator", {
478-
start: eqIndex,
479-
end: eqIndex + 1,
480-
});
481-
if (quote) {
482-
// "
483-
ctx.addToken("Punctuator", {
484-
start: valueStartIndex,
485-
end: literalStartIndex,
442+
if (ctx.code[valueStartIndex] === "{") {
443+
// Svelte v5 `this={"..."}`
444+
const openingQuoteIndex = indexOf(
445+
ctx.code,
446+
(c) => c === '"' || c === "'",
447+
valueStartIndex + 1,
448+
);
449+
const quote = ctx.code[openingQuoteIndex];
450+
const closingQuoteIndex = indexOf(
451+
ctx.code,
452+
(c) => c === quote,
453+
openingQuoteIndex + thisValue.length,
454+
);
455+
const closeIndex = ctx.code.indexOf("}", closingQuoteIndex + 1);
456+
const endIndex = indexOf(
457+
ctx.code,
458+
(c) => c === ">" || !c.trim(),
459+
closeIndex,
460+
);
461+
thisNode = createSvelteSpecialDirective(startIndex, endIndex, eqIndex, {
462+
type: "Literal",
463+
value: thisValue,
464+
range: [openingQuoteIndex, closingQuoteIndex + 1],
486465
});
487-
}
488-
ctx.addToken("HTMLText", {
489-
start: literalStartIndex,
490-
end: literalEndIndex,
491-
});
492-
if (quote) {
493-
// "
466+
} else {
467+
const quote = ctx.code.startsWith(thisValue, valueStartIndex)
468+
? null
469+
: ctx.code[valueStartIndex];
470+
const literalStartIndex = quote
471+
? valueStartIndex + quote.length
472+
: valueStartIndex;
473+
const literalEndIndex = literalStartIndex + thisValue.length;
474+
const endIndex = quote ? literalEndIndex + quote.length : literalEndIndex;
475+
const thisAttr: SvelteAttribute = {
476+
type: "SvelteAttribute",
477+
key: null as any,
478+
boolean: false,
479+
value: [],
480+
parent: element.startTag,
481+
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
482+
};
483+
thisAttr.key = {
484+
type: "SvelteName",
485+
name: "this",
486+
parent: thisAttr,
487+
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
488+
};
489+
thisAttr.value.push({
490+
type: "SvelteLiteral",
491+
value: thisValue,
492+
parent: thisAttr,
493+
...ctx.getConvertLocation({
494+
start: literalStartIndex,
495+
end: literalEndIndex,
496+
}),
497+
});
498+
// this
499+
ctx.addToken("HTMLIdentifier", {
500+
start: startIndex,
501+
end: startIndex + 4,
502+
});
503+
// =
494504
ctx.addToken("Punctuator", {
495-
start: literalEndIndex,
496-
end: endIndex,
505+
start: eqIndex,
506+
end: eqIndex + 1,
507+
});
508+
if (quote) {
509+
// "
510+
ctx.addToken("Punctuator", {
511+
start: valueStartIndex,
512+
end: literalStartIndex,
513+
});
514+
}
515+
ctx.addToken("HTMLText", {
516+
start: literalStartIndex,
517+
end: literalEndIndex,
497518
});
519+
if (quote) {
520+
// "
521+
ctx.addToken("Punctuator", {
522+
start: literalEndIndex,
523+
end: endIndex,
524+
});
525+
}
526+
thisNode = thisAttr;
498527
}
499-
thisNode = thisAttr;
500528
} else {
501529
// this={...}
502530
const eqIndex = ctx.code.lastIndexOf("=", getWithLoc(thisValue).start);
@@ -507,6 +535,30 @@ function processThisAttribute(
507535
(c) => c === ">" || !c.trim(),
508536
closeIndex,
509537
);
538+
thisNode = createSvelteSpecialDirective(
539+
startIndex,
540+
endIndex,
541+
eqIndex,
542+
thisValue,
543+
);
544+
}
545+
546+
const targetIndex = element.startTag.attributes.findIndex(
547+
(attr) => thisNode.range[1] <= attr.range[0],
548+
);
549+
if (targetIndex === -1) {
550+
element.startTag.attributes.push(thisNode);
551+
} else {
552+
element.startTag.attributes.splice(targetIndex, 0, thisNode);
553+
}
554+
555+
/** Create SvelteSpecialDirective */
556+
function createSvelteSpecialDirective(
557+
startIndex: number,
558+
endIndex: number,
559+
eqIndex: number,
560+
expression: ESTree.Expression,
561+
): SvelteSpecialDirective {
510562
const thisDir: SvelteSpecialDirective = {
511563
type: "SvelteSpecialDirective",
512564
kind: "this",
@@ -530,19 +582,11 @@ function processThisAttribute(
530582
start: eqIndex,
531583
end: eqIndex + 1,
532584
});
533-
ctx.scriptLet.addExpression(thisValue, thisDir, null, (es) => {
585+
ctx.scriptLet.addExpression(expression, thisDir, null, (es) => {
534586
thisDir.expression = es;
535587
});
536-
thisNode = thisDir;
537-
}
538588

539-
const targetIndex = element.startTag.attributes.findIndex(
540-
(attr) => thisNode.range[1] <= attr.range[0],
541-
);
542-
if (targetIndex === -1) {
543-
element.startTag.attributes.push(thisNode);
544-
} else {
545-
element.startTag.attributes.splice(targetIndex, 0, thisNode);
589+
return thisDir;
546590
}
547591
}
548592

src/parser/svelte-ast-types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export interface Title extends BaseNode {
162162
export interface Options extends BaseNode {
163163
type: "Options";
164164
name: "svelte:options";
165-
children: TemplateNode[];
165+
children?: TemplateNode[]; // This property does not exist in Svelte v5.
166166
attributes: AttributeOrDirective[];
167167
}
168168
export interface SlotTemplate extends BaseNode {

src/parser/template.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function parseTemplate(
2121
try {
2222
const svelteAst = parse(code, {
2323
filename: parserOptions.filePath,
24-
}) as SvAST.Ast;
24+
}) as never as SvAST.Ast;
2525
const ast = convertSvelteRoot(svelteAst, ctx);
2626
sortNodes(ast.body);
2727

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"FIXME": "As of now, Svelte v5 gives an error with single-line await blocks.",
3+
"parse": {
4+
"svelte": "^4 || ^3"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"parse": {
3+
"svelte": "^4 || ^3"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<svelte:options tag="my-custom-element"/>
1+
<svelte:options customElement="my-custom-element"/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<svelte:options tag="my-custom-element"/>

0 commit comments

Comments
 (0)