Skip to content

Commit 02e4910

Browse files
committed
feat: makes it optional whether to parse runes.
1 parent 249deb6 commit 02e4910

File tree

18 files changed

+982
-49
lines changed

18 files changed

+982
-49
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ export default [
259259
parser: svelteParser,
260260
parserOptions: {
261261
svelteFeatures: {
262+
/* -- Experimental Svelte Features -- */
263+
/* It may be changed or removed in minor versions without notice. */
264+
// If true, it will analyze Runes.
265+
// By default, it will try to read `compilerOptions.runes` from `svelte.config.js`.
266+
// However, note that if it cannot be resolved due to static analysis, it will behave as false.
267+
runes: false,
262268
/* -- Experimental Svelte Features -- */
263269
/* It may be changed or removed in minor versions without notice. */
264270
// Whether to parse the `generics` attribute.
@@ -278,6 +284,12 @@ For example in `.eslintrc.*`:
278284
"parser": "svelte-eslint-parser",
279285
"parserOptions": {
280286
"svelteFeatures": {
287+
/* -- Experimental Svelte Features -- */
288+
/* It may be changed or removed in minor versions without notice. */
289+
// If true, it will analyze Runes.
290+
// By default, it will try to read `compilerOptions.runes` from `svelte.config.js`.
291+
// However, note that if it cannot be resolved due to static analysis, it will behave as false.
292+
"runes": false,
281293
/* -- Experimental Svelte Features -- */
282294
/* It may be changed or removed in minor versions without notice. */
283295
// Whether to parse the `generics` attribute.
@@ -292,7 +304,7 @@ For example in `.eslintrc.*`:
292304

293305
**_This is an experimental feature. It may be changed or removed in minor versions without notice._**
294306

295-
If you install Svelte v5 the parser will be able to parse runes, and will also be able to parse `*.js` and `*.ts` files.
307+
If you install Svelte v5 and turn on runes (`compilerOptions.runes` in `svelte.config.js` or `parserOptions.svelteFeatures.runes` in ESLint config is `true`), the parser will be able to parse runes, and will also be able to parse `*.js` and `*.ts` files.
296308

297309
When using this mode in an ESLint configuration, it is recommended to set it per file pattern as below.
298310

src/parser/analyze-scope.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
import { addReference, addVariable, getScopeFromNode } from "../scope";
1111
import { addElementToSortedArray } from "../utils";
1212
import type { NormalizedParserOptions } from "./parser-options";
13+
import type { SvelteParseContext } from "./svelte-parse-context";
1314
/**
1415
* Analyze scope
1516
*/
@@ -160,6 +161,7 @@ export function analyzeStoreScope(scopeManager: ScopeManager): void {
160161
export function analyzePropsScope(
161162
body: SvelteScriptElement,
162163
scopeManager: ScopeManager,
164+
svelteParseContext: SvelteParseContext,
163165
): void {
164166
const moduleScope = scopeManager.scopes.find(
165167
(scope) => scope.type === "module",
@@ -187,23 +189,25 @@ export function analyzePropsScope(
187189
}
188190
}
189191
} else if (node.type === "VariableDeclaration") {
190-
// Process for Svelte v5 Runes props. e.g. `let { x = $bindable() } = $props()`;
191-
for (const decl of node.declarations) {
192-
if (
193-
decl.init?.type === "CallExpression" &&
194-
decl.init.callee.type === "Identifier" &&
195-
decl.init.callee.name === "$props" &&
196-
decl.id.type === "ObjectPattern"
197-
) {
198-
for (const pattern of extractPattern(decl.id)) {
199-
if (
200-
pattern.type === "AssignmentPattern" &&
201-
pattern.left.type === "Identifier" &&
202-
pattern.right.type === "CallExpression" &&
203-
pattern.right.callee.type === "Identifier" &&
204-
pattern.right.callee.name === "$bindable"
205-
) {
206-
addPropReference(pattern.left, moduleScope);
192+
if (svelteParseContext.runes) {
193+
// Process for Svelte v5 Runes props. e.g. `let { x = $bindable() } = $props()`;
194+
for (const decl of node.declarations) {
195+
if (
196+
decl.init?.type === "CallExpression" &&
197+
decl.init.callee.type === "Identifier" &&
198+
decl.init.callee.name === "$props" &&
199+
decl.id.type === "ObjectPattern"
200+
) {
201+
for (const pattern of extractPattern(decl.id)) {
202+
if (
203+
pattern.type === "AssignmentPattern" &&
204+
pattern.left.type === "Identifier" &&
205+
pattern.right.type === "CallExpression" &&
206+
pattern.right.callee.type === "Identifier" &&
207+
pattern.right.callee.name === "$bindable"
208+
) {
209+
addPropReference(pattern.left, moduleScope);
210+
}
207211
}
208212
}
209213
}

src/parser/globals.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { svelteVersion } from "./svelte-version";
1+
import type { SvelteParseContext } from "./svelte-parse-context";
22

3-
const globalsForSvelte4 = ["$$slots", "$$props", "$$restProps"] as const;
3+
const globalsForSvelte = ["$$slots", "$$props", "$$restProps"] as const;
44
export const globalsForRunes = [
55
"$state",
66
"$derived",
@@ -10,10 +10,22 @@ export const globalsForRunes = [
1010
"$inspect",
1111
"$host",
1212
] as const;
13-
const globalsForSvelte5 = [...globalsForSvelte4, ...globalsForRunes];
14-
export const globals = svelteVersion.gte(5)
15-
? globalsForSvelte5
16-
: globalsForSvelte4;
17-
export const globalsForSvelteScript = svelteVersion.gte(5)
18-
? globalsForRunes
19-
: [];
13+
type Global =
14+
| (typeof globalsForSvelte)[number]
15+
| (typeof globalsForRunes)[number];
16+
export function getGlobalsForSvelte(
17+
svelteParseContext: SvelteParseContext,
18+
): readonly Global[] {
19+
if (svelteParseContext.runes) {
20+
return [...globalsForSvelte, ...globalsForRunes];
21+
}
22+
return globalsForSvelte;
23+
}
24+
export function getGlobalsForSvelteScript(
25+
svelteParseContext: SvelteParseContext,
26+
): readonly Global[] {
27+
if (svelteParseContext.runes) {
28+
return globalsForRunes;
29+
}
30+
return [];
31+
}

src/parser/index.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@ import {
3434
styleNodeLoc,
3535
styleNodeRange,
3636
} from "./style-context";
37-
import { globals, globalsForSvelteScript } from "./globals";
38-
import { svelteVersion } from "./svelte-version";
37+
import { getGlobalsForSvelte, getGlobalsForSvelteScript } from "./globals";
3938
import type { NormalizedParserOptions } from "./parser-options";
4039
import { isTypeScript, normalizeParserOptions } from "./parser-options";
4140
import { getFragmentFromRoot } from "./compat";
41+
import {
42+
isEnableRunes,
43+
resolveSvelteParseContextForSvelte,
44+
resolveSvelteParseContextForSvelteScript,
45+
type SvelteParseContext,
46+
} from "./svelte-parse-context";
47+
import type { StaticSvelteConfig } from "../svelte-config";
48+
import { resolveSvelteConfig } from "../svelte-config";
4249

4350
export {
4451
StyleContext,
@@ -74,8 +81,13 @@ type ParseResult = {
7481
isSvelteScript: false;
7582
getSvelteHtmlAst: () => SvAST.Fragment | Compiler.Fragment;
7683
getStyleContext: () => StyleContext;
84+
svelteParseContext: SvelteParseContext;
85+
}
86+
| {
87+
isSvelte: false;
88+
isSvelteScript: true;
89+
svelteParseContext: SvelteParseContext;
7790
}
78-
| { isSvelte: false; isSvelteScript: true }
7991
);
8092
visitorKeys: { [type: string]: string[] };
8193
scopeManager: ScopeManager;
@@ -84,29 +96,35 @@ type ParseResult = {
8496
* Parse source code
8597
*/
8698
export function parseForESLint(code: string, options?: any): ParseResult {
99+
const svelteConfig = resolveSvelteConfig(options?.filePath);
87100
const parserOptions = normalizeParserOptions(options);
88101

89102
if (
90-
svelteVersion.hasRunes &&
103+
isEnableRunes(svelteConfig, parserOptions) &&
91104
parserOptions.filePath &&
92105
!parserOptions.filePath.endsWith(".svelte") &&
93106
// If no `filePath` is set in ESLint, "<input>" will be specified.
94107
parserOptions.filePath !== "<input>"
95108
) {
96109
const trimmed = code.trim();
97110
if (!trimmed.startsWith("<") && !trimmed.endsWith(">")) {
98-
return parseAsScript(code, parserOptions);
111+
const svelteParseContext = resolveSvelteParseContextForSvelteScript(
112+
svelteConfig,
113+
parserOptions,
114+
);
115+
return parseAsScript(code, parserOptions, svelteParseContext);
99116
}
100117
}
101118

102-
return parseAsSvelte(code, parserOptions);
119+
return parseAsSvelte(code, svelteConfig, parserOptions);
103120
}
104121

105122
/**
106123
* Parse source code as svelte component
107124
*/
108125
function parseAsSvelte(
109126
code: string,
127+
svelteConfig: StaticSvelteConfig | null,
110128
parserOptions: NormalizedParserOptions,
111129
): ParseResult {
112130
const ctx = new Context(code, parserOptions);
@@ -115,14 +133,19 @@ function parseAsSvelte(
115133
ctx,
116134
parserOptions,
117135
);
136+
const svelteParseContext = resolveSvelteParseContextForSvelte(
137+
svelteConfig,
138+
parserOptions,
139+
resultTemplate.svelteAst,
140+
);
118141

119142
const scripts = ctx.sourceCode.scripts;
120143
const resultScript = ctx.isTypeScript()
121144
? parseTypeScriptInSvelte(
122145
scripts.getCurrentVirtualCodeInfo(),
123146
scripts.attrs,
124147
parserOptions,
125-
{ slots: ctx.slots },
148+
{ slots: ctx.slots, svelteParseContext },
126149
)
127150
: parseScriptInSvelte(
128151
scripts.getCurrentVirtualCode(),
@@ -141,7 +164,10 @@ function parseAsSvelte(
141164
analyzeSnippetsScope(ctx.snippets, resultScript.scopeManager!);
142165

143166
// Add $$xxx variable
144-
addGlobalVariables(resultScript.scopeManager!, globals);
167+
addGlobalVariables(
168+
resultScript.scopeManager!,
169+
getGlobalsForSvelte(svelteParseContext),
170+
);
145171

146172
const ast = resultTemplate.ast;
147173

@@ -177,7 +203,7 @@ function parseAsSvelte(
177203
attr.value[0].value === "module",
178204
)
179205
) {
180-
analyzePropsScope(body, resultScript.scopeManager!);
206+
analyzePropsScope(body, resultScript.scopeManager!, svelteParseContext);
181207
}
182208
}
183209
if (statements.length) {
@@ -208,6 +234,7 @@ function parseAsSvelte(
208234
},
209235
styleNodeLoc,
210236
styleNodeRange,
237+
svelteParseContext,
211238
});
212239
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
213240

@@ -220,18 +247,23 @@ function parseAsSvelte(
220247
function parseAsScript(
221248
code: string,
222249
parserOptions: NormalizedParserOptions,
250+
svelteParseContext: SvelteParseContext,
223251
): ParseResult {
224252
const lang = parserOptions.filePath?.split(".").pop();
225253
const resultScript = isTypeScript(parserOptions, lang)
226-
? parseTypeScript(code, { lang }, parserOptions)
254+
? parseTypeScript(code, { lang }, parserOptions, svelteParseContext)
227255
: parseScript(code, { lang }, parserOptions);
228256

229257
// Add $$xxx variable
230-
addGlobalVariables(resultScript.scopeManager!, globalsForSvelteScript);
258+
addGlobalVariables(
259+
resultScript.scopeManager!,
260+
getGlobalsForSvelteScript(svelteParseContext),
261+
);
231262

232263
resultScript.services = Object.assign(resultScript.services || {}, {
233264
isSvelte: false,
234265
isSvelteScript: true,
266+
svelteParseContext,
235267
});
236268
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);
237269
return resultScript as any;

src/parser/parser-options.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export type NormalizedParserOptions = {
2020
[key: string]: any;
2121
};
2222
svelteFeatures?: {
23+
// If true, it will analyze Runes.
24+
// By default, it will try to read `compilerOptions.runes` from `svelte.config.js`.
25+
// However, note that if it cannot be resolved due to static analysis, it will behave as false.
26+
runes?: boolean;
2327
/* -- Experimental Svelte Features -- */
2428
// Whether to parse the `generics` attribute.
2529
// See https://github.com/sveltejs/rfcs/pull/38

src/parser/svelte-parse-context.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type * as Compiler from "svelte/compiler";
2+
import type * as SvAST from "./svelte-ast-types";
3+
import type { NormalizedParserOptions } from "./parser-options";
4+
import { compilerVersion, svelteVersion } from "./svelte-version";
5+
import type { StaticSvelteConfig } from "../svelte-config";
6+
7+
/** The context for parsing. */
8+
export type SvelteParseContext = {
9+
/**
10+
* Whether to use Runes mode.
11+
* May be `true` if the user is using Svelte v5.
12+
* Resolved from `svelte.config.js` or `parserOptions`, but may be overridden by `<svelte:options>`.
13+
*/
14+
runes: boolean;
15+
/** The version of "svelte/compiler". */
16+
compilerVersion: string;
17+
/** The result of static analysis of `svelte.config.js`. */
18+
svelteConfig: StaticSvelteConfig | null;
19+
};
20+
21+
export function isEnableRunes(
22+
svelteConfig: StaticSvelteConfig | null,
23+
parserOptions: NormalizedParserOptions,
24+
): boolean {
25+
if (!svelteVersion.gte(5)) return false;
26+
if (parserOptions.svelteFeatures?.runes != null) {
27+
return Boolean(parserOptions.svelteFeatures.runes);
28+
} else if (svelteConfig?.compilerOptions?.runes != null) {
29+
return Boolean(svelteConfig.compilerOptions.runes);
30+
}
31+
return false;
32+
}
33+
34+
export function resolveSvelteParseContextForSvelte(
35+
svelteConfig: StaticSvelteConfig | null,
36+
parserOptions: NormalizedParserOptions,
37+
svelteAst: Compiler.Root | SvAST.AstLegacy,
38+
): SvelteParseContext {
39+
const svelteOptions = (svelteAst as Compiler.Root).options;
40+
if (svelteOptions?.runes != null) {
41+
return {
42+
runes: svelteOptions.runes,
43+
compilerVersion,
44+
svelteConfig,
45+
};
46+
}
47+
48+
return {
49+
runes: isEnableRunes(svelteConfig, parserOptions),
50+
compilerVersion,
51+
svelteConfig,
52+
};
53+
}
54+
55+
export function resolveSvelteParseContextForSvelteScript(
56+
svelteConfig: StaticSvelteConfig | null,
57+
parserOptions: NormalizedParserOptions,
58+
): SvelteParseContext {
59+
return resolveSvelteParseContext(svelteConfig, parserOptions);
60+
}
61+
62+
function resolveSvelteParseContext(
63+
svelteConfig: StaticSvelteConfig | null,
64+
parserOptions: NormalizedParserOptions,
65+
): SvelteParseContext {
66+
return {
67+
runes: isEnableRunes(svelteConfig, parserOptions),
68+
compilerVersion,
69+
svelteConfig,
70+
};
71+
}

src/parser/svelte-version.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { VERSION as SVELTE_VERSION } from "svelte/compiler";
1+
import { VERSION as compilerVersion } from "svelte/compiler";
22

3-
const verStrings = SVELTE_VERSION.split(".");
3+
export { compilerVersion };
4+
5+
const verStrings = compilerVersion.split(".");
46

57
export const svelteVersion = {
68
gte(v: number): boolean {
79
return Number(verStrings[0]) >= v;
810
},
9-
hasRunes: Number(verStrings[0]) >= 5,
1011
};

0 commit comments

Comments
 (0)