Skip to content

Commit 7c9657c

Browse files
committed
feat: add svelteFeatures.runes option and add rune symbols to global scope
1 parent 80fb13f commit 7c9657c

File tree

61 files changed

+2605
-1951
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2605
-1951
lines changed

README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,22 @@ module.exports = {
169169
}
170170
```
171171

172-
### parserOptions.runes
172+
### parserOptions.svelteFeatures
173+
174+
You can use `parserOptions.svelteFeatures` property to specify how to parse related to Svelte features. For example:
175+
176+
```json
177+
{
178+
"parser": "svelte-eslint-parser",
179+
"parserOptions": {
180+
"svelteFeatures": {
181+
"runes": false
182+
}
183+
}
184+
}
185+
```
186+
187+
#### parserOptions.svelteFeatures.runes
173188

174189
***This is an experimental feature. It may be changed or removed in minor versions without notice.***
175190

@@ -179,7 +194,9 @@ If set to `true`, Rune symbols will be parsed. In this mode, the parser also par
179194
{
180195
"parser": "svelte-eslint-parser",
181196
"parserOptions": {
182-
"runes": true
197+
"svelteFeatures": {
198+
"runes": true // Default `false`
199+
}
183200
}
184201
}
185202
```
@@ -193,7 +210,9 @@ When using this mode in an ESLint configuration, it is recommended to set it per
193210
"files": ["*.svelte"],
194211
"parser": "svelte-eslint-parser",
195212
"parserOptions": {
196-
"runes": true,
213+
"svelteFeatures": {
214+
"runes": true,
215+
},
197216
"parser": "...",
198217
...
199218
}
@@ -202,15 +221,19 @@ When using this mode in an ESLint configuration, it is recommended to set it per
202221
"files": ["*.svelte.js"],
203222
"parser": "svelte-eslint-parser",
204223
"parserOptions": {
205-
"runes": true,
224+
"svelteFeatures": {
225+
"runes": true,
226+
},
206227
...
207228
}
208229
},
209230
{
210231
"files": ["*.svelte.ts"],
211232
"parser": "svelte-eslint-parser",
212233
"parserOptions": {
213-
"runes": true,
234+
"svelteFeatures": {
235+
"runes": true,
236+
},
214237
"parser": "...(ts parser)...",
215238
...
216239
}
@@ -219,6 +242,18 @@ When using this mode in an ESLint configuration, it is recommended to set it per
219242
}
220243
```
221244

245+
Even if `runes` is not set to `true`, if it is enabled in the `<svelte:option>` of the `*.svelte` file, it will be parsed as `runes` mode is enabled
246+
247+
```svelte
248+
<svelte:options runes={true} />
249+
```
250+
251+
Also, even if `runes` is set to `true`, if it is disabled in the `<svelte:option>` of the `*.svelte` file, it will be parsed as `runes` mode is disabled.
252+
253+
```svelte
254+
<svelte:options runes={false} />
255+
```
256+
222257
## :computer: Editor Integrations
223258

224259
### Visual Studio Code

src/context/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export class Context {
136136

137137
public readonly slots = new Set<SvelteHTMLElement>();
138138

139+
public runes: boolean | null = null;
140+
139141
public readonly elements = new Map<
140142
SvelteElement,
141143
| SvAST.InlineComponent

src/parser/analyze-scope.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,30 @@ export function analyzePropsScope(
217217
}
218218
}
219219

220+
/** Analyze Runes. e.g. $state() */
221+
export function analyzeRunesScope(scopeManager: ScopeManager): void {
222+
const globalScope = scopeManager.globalScope;
223+
// https://svelte-5-preview.vercel.app/docs/runes
224+
for (const $name of ["$state", "$derived", "$effect", "$props"]) {
225+
if (globalScope.set.has($name)) continue;
226+
const variable = new Variable();
227+
variable.name = $name;
228+
(variable as any).scope = globalScope;
229+
globalScope.variables.push(variable);
230+
globalScope.set.set($name, variable);
231+
globalScope.through = globalScope.through.filter((reference) => {
232+
if (reference.identifier.name === $name) {
233+
// Links the variable and the reference.
234+
// And this reference is removed from `Scope#through`.
235+
reference.resolved = variable;
236+
addReference(variable.references, reference);
237+
return false;
238+
}
239+
return true;
240+
});
241+
}
242+
}
243+
220244
/** Remove reference from through */
221245
function removeReferenceFromThrough(reference: Reference, baseScope: Scope) {
222246
const variable = reference.resolved!;

src/parser/converts/element.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,27 @@ function convertOptionsElement(
810810
parent: SvelteSpecialElement["parent"],
811811
ctx: Context,
812812
): SvelteSpecialElement {
813-
return convertSpecialElement(node, parent, ctx);
813+
const element = convertSpecialElement(node, parent, ctx);
814+
815+
// Extract rune mode from options.
816+
for (const attr of node.attributes) {
817+
if (attr.type === "Attribute" && attr.name === "runes") {
818+
if (attr.value === true) {
819+
ctx.runes = true;
820+
} else if (attr.value.length === 1) {
821+
const val = attr.value[0];
822+
if (
823+
val.type === "MustacheTag" &&
824+
val.expression.type === "Literal" &&
825+
typeof val.expression.value === "boolean"
826+
) {
827+
ctx.runes = val.expression.value;
828+
}
829+
}
830+
}
831+
}
832+
833+
return element;
814834
}
815835

816836
/** Convert for <svelte:fragment> element. */

src/parser/index.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { parseTemplate } from "./template";
1717
import {
1818
analyzePropsScope,
1919
analyzeReactiveScope,
20+
analyzeRunesScope,
2021
analyzeStoreScope,
2122
} from "./analyze-scope";
2223
import { ParseError } from "../errors";
@@ -64,11 +65,11 @@ type ParseResult = {
6465
(
6566
| {
6667
isSvelte: true;
67-
svelteRunes: boolean;
68+
svelteOptions: { runes: boolean };
6869
getSvelteHtmlAst: () => SvAST.Fragment;
6970
getStyleContext: () => StyleContext;
7071
}
71-
| { isSvelte: false; svelteRunes: boolean }
72+
| { isSvelte: false; svelteOptions: { runes: boolean } }
7273
);
7374
visitorKeys: { [type: string]: string[] };
7475
scopeManager: ScopeManager;
@@ -82,7 +83,7 @@ export function parseForESLint(code: string, options?: any): ParseResult {
8283
if (
8384
parserOptions.filePath &&
8485
!parserOptions.filePath.endsWith(".svelte") &&
85-
parserOptions.runes
86+
parserOptions.svelteFeatures.runes
8687
) {
8788
const trimmed = code.trim();
8889
if (!trimmed.startsWith("<") && !trimmed.endsWith(">")) {
@@ -107,6 +108,8 @@ function parseAsSvelte(
107108
parserOptions,
108109
);
109110

111+
const runes = ctx.runes ?? parserOptions.svelteFeatures.runes;
112+
110113
const scripts = ctx.sourceCode.scripts;
111114
const resultScript = ctx.isTypeScript()
112115
? parseTypeScript(
@@ -131,8 +134,9 @@ function parseAsSvelte(
131134
analyzeStoreScope(resultScript.scopeManager!); // for reactive vars
132135

133136
// Add $$xxx variable
137+
const globalScope = resultScript.scopeManager!.globalScope;
134138
for (const $$name of ["$$slots", "$$props", "$$restProps"]) {
135-
const globalScope = resultScript.scopeManager!.globalScope;
139+
if (globalScope.set.has($$name)) continue;
136140
const variable = new Variable();
137141
variable.name = $$name;
138142
(variable as any).scope = globalScope;
@@ -150,6 +154,10 @@ function parseAsSvelte(
150154
});
151155
}
152156

157+
if (runes) {
158+
analyzeRunesScope(resultScript.scopeManager!);
159+
}
160+
153161
const ast = resultTemplate.ast;
154162

155163
const statements = [...resultScript.ast.body];
@@ -203,6 +211,7 @@ function parseAsSvelte(
203211
resultScript.ast = ast as any;
204212
resultScript.services = Object.assign(resultScript.services || {}, {
205213
isSvelte: true,
214+
svelteOptions: { runes },
206215
getSvelteHtmlAst() {
207216
return resultTemplate.svelteAst.html;
208217
},
@@ -228,11 +237,11 @@ function parseAsScript(
228237
parserOptions: NormalizedParserOptions,
229238
): ParseResult {
230239
const lang = parserOptions.filePath?.split(".").pop() || "js";
231-
// TODO support runes
232240
const resultScript = parseScript(code, { lang }, parserOptions);
241+
analyzeRunesScope(resultScript.scopeManager!);
233242
resultScript.services = Object.assign(resultScript.services || {}, {
234243
isSvelte: false,
235-
runes: parserOptions.runes,
244+
svelteOptions: { runes: true },
236245
});
237246
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
238247
return resultScript as any;
@@ -248,7 +257,7 @@ type NormalizedParserOptions = {
248257
comment: boolean;
249258
eslintVisitorKeys: boolean;
250259
eslintScopeManager: boolean;
251-
runes: boolean;
260+
svelteFeatures: { runes: boolean };
252261
filePath?: string;
253262
};
254263

@@ -264,7 +273,10 @@ function normalizeParserOptions(options: any): NormalizedParserOptions {
264273
comment: true,
265274
eslintVisitorKeys: true,
266275
eslintScopeManager: true,
267-
rune: false,
276+
svelteFeatures: {
277+
rune: false,
278+
...(options?.svelteFeatures || {}),
279+
},
268280
...(options || {}),
269281
};
270282
parserOptions.sourceType = "module";
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
{
2-
"runes": true
2+
"svelteFeatures": {
3+
"runes": true
4+
}
35
}

tests/fixtures/parser/ast/svelte-5-preview/docs/fine-grained-reactivity/example01-no-undef-result.json

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)