Skip to content

Commit faf9e47

Browse files
committed
feat: add svelteFeatures.runes option and add rune symbols to global scope
1 parent 7338187 commit faf9e47

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";
@@ -65,11 +66,11 @@ type ParseResult = {
6566
(
6667
| {
6768
isSvelte: true;
68-
svelteRunes: boolean;
69+
svelteOptions: { runes: boolean };
6970
getSvelteHtmlAst: () => SvAST.Fragment;
7071
getStyleContext: () => StyleContext;
7172
}
72-
| { isSvelte: false; svelteRunes: boolean }
73+
| { isSvelte: false; svelteOptions: { runes: boolean } }
7374
);
7475
visitorKeys: { [type: string]: string[] };
7576
scopeManager: ScopeManager;
@@ -83,7 +84,7 @@ export function parseForESLint(code: string, options?: any): ParseResult {
8384
if (
8485
parserOptions.filePath &&
8586
!parserOptions.filePath.endsWith(".svelte") &&
86-
parserOptions.runes
87+
parserOptions.svelteFeatures.runes
8788
) {
8889
const trimmed = code.trim();
8990
if (!trimmed.startsWith("<") && !trimmed.endsWith(">")) {
@@ -108,6 +109,8 @@ function parseAsSvelte(
108109
parserOptions,
109110
);
110111

112+
const runes = ctx.runes ?? parserOptions.svelteFeatures.runes;
113+
111114
const scripts = ctx.sourceCode.scripts;
112115
const resultScript = ctx.isTypeScript()
113116
? parseTypeScript(
@@ -132,8 +135,9 @@ function parseAsSvelte(
132135
analyzeStoreScope(resultScript.scopeManager!); // for reactive vars
133136

134137
// Add $$xxx variable
138+
const globalScope = resultScript.scopeManager!.globalScope;
135139
for (const $$name of globals) {
136-
const globalScope = resultScript.scopeManager!.globalScope;
140+
if (globalScope.set.has($$name)) continue;
137141
const variable = new Variable();
138142
variable.name = $$name;
139143
(variable as any).scope = globalScope;
@@ -151,6 +155,10 @@ function parseAsSvelte(
151155
});
152156
}
153157

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

156164
const statements = [...resultScript.ast.body];
@@ -204,6 +212,7 @@ function parseAsSvelte(
204212
resultScript.ast = ast as any;
205213
resultScript.services = Object.assign(resultScript.services || {}, {
206214
isSvelte: true,
215+
svelteOptions: { runes },
207216
getSvelteHtmlAst() {
208217
return resultTemplate.svelteAst.html;
209218
},
@@ -229,11 +238,11 @@ function parseAsScript(
229238
parserOptions: NormalizedParserOptions,
230239
): ParseResult {
231240
const lang = parserOptions.filePath?.split(".").pop() || "js";
232-
// TODO support runes
233241
const resultScript = parseScript(code, { lang }, parserOptions);
242+
analyzeRunesScope(resultScript.scopeManager!);
234243
resultScript.services = Object.assign(resultScript.services || {}, {
235244
isSvelte: false,
236-
runes: parserOptions.runes,
245+
svelteOptions: { runes: true },
237246
});
238247
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
239248
return resultScript as any;
@@ -249,7 +258,7 @@ type NormalizedParserOptions = {
249258
comment: boolean;
250259
eslintVisitorKeys: boolean;
251260
eslintScopeManager: boolean;
252-
runes: boolean;
261+
svelteFeatures: { runes: boolean };
253262
filePath?: string;
254263
};
255264

@@ -265,7 +274,10 @@ function normalizeParserOptions(options: any): NormalizedParserOptions {
265274
comment: true,
266275
eslintVisitorKeys: true,
267276
eslintScopeManager: true,
268-
rune: false,
277+
svelteFeatures: {
278+
rune: false,
279+
...(options?.svelteFeatures || {}),
280+
},
269281
...(options || {}),
270282
};
271283
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)