Skip to content

Commit 513806c

Browse files
authored
fix: allow children in valid-prop-names-in-kit-pages rule (#1048)
1 parent f79fded commit 513806c

File tree

11 files changed

+183
-35
lines changed

11 files changed

+183
-35
lines changed

.changeset/gold-bottles-learn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix: allow `children` in `valid-prop-names-in-kit-pages` rule

packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@ import type { AST } from 'svelte-eslint-parser';
22
import type { TSESTree } from '@typescript-eslint/types';
33
import { createRule } from '../utils/index.js';
44
import type { RuleContext } from '../types.js';
5+
import { getSvelteVersion } from '../utils/svelte-context.js';
6+
import { getFilename } from '../utils/compat.js';
57

68
const EXPECTED_PROP_NAMES = ['data', 'errors', 'form', 'snapshot'];
9+
const EXPECTED_PROP_NAMES_SVELTE5 = [...EXPECTED_PROP_NAMES, 'children'];
710

8-
function checkProp(node: TSESTree.VariableDeclarator, context: RuleContext) {
11+
function checkProp(
12+
node: TSESTree.VariableDeclarator,
13+
context: RuleContext,
14+
expectedPropNames: string[]
15+
) {
916
if (node.id.type !== 'ObjectPattern') return;
1017
for (const p of node.id.properties) {
1118
if (
1219
p.type === 'Property' &&
1320
p.value.type === 'Identifier' &&
14-
!EXPECTED_PROP_NAMES.includes(p.value.name)
21+
!expectedPropNames.includes(p.value.name)
1522
) {
1623
context.report({
1724
node: p.value,
@@ -42,6 +49,8 @@ export default createRule('valid-prop-names-in-kit-pages', {
4249
},
4350
create(context) {
4451
let isScript = false;
52+
const isSvelte5 = getSvelteVersion(getFilename(context)) === '5';
53+
const expectedPropNames = isSvelte5 ? EXPECTED_PROP_NAMES_SVELTE5 : EXPECTED_PROP_NAMES;
4554
return {
4655
// <script>
4756
'Program > SvelteScriptElement > SvelteStartTag': (node: AST.SvelteStartTag) => {
@@ -67,7 +76,7 @@ export default createRule('valid-prop-names-in-kit-pages', {
6776

6877
// export let foo
6978
if (node.id.type === 'Identifier') {
70-
if (!EXPECTED_PROP_NAMES.includes(node.id.name)) {
79+
if (!expectedPropNames.includes(node.id.name)) {
7180
context.report({
7281
node,
7382
loc: node.loc,
@@ -78,7 +87,7 @@ export default createRule('valid-prop-names-in-kit-pages', {
7887
}
7988

8089
// export let { xxx, yyy } = zzz
81-
checkProp(node, context);
90+
checkProp(node, context, expectedPropNames);
8291
},
8392

8493
// Svelte5
@@ -93,7 +102,7 @@ export default createRule('valid-prop-names-in-kit-pages', {
93102
return;
94103
}
95104

96-
checkProp(node, context);
105+
checkProp(node, context, expectedPropNames);
97106
}
98107
};
99108
}

packages/eslint-plugin-svelte/src/utils/svelte-context.ts

+97-29
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs';
33
import path from 'path';
44
import { getPackageJsons } from './get-package-json.js';
55
import { getFilename, getSourceCode } from './compat.js';
6+
import { createCache } from './cache.js';
67

78
const isRunInBrowser = !fs.readFileSync;
89

@@ -96,23 +97,51 @@ function getSvelteKitFileTypeFromFilePath(filePath: string): SvelteContext['svel
9697
}
9798
}
9899

100+
function extractMajorVersion(version: string, recognizePrereleaseVersion: boolean): string | null {
101+
if (recognizePrereleaseVersion) {
102+
const match = /^(?:\^|~)?(\d+\.0\.0-next)/.exec(version);
103+
if (match && match[1]) {
104+
return match[1];
105+
}
106+
}
107+
108+
const match = /^(?:\^|~)?(\d+)\./.exec(version);
109+
if (match && match[1]) {
110+
return match[1];
111+
}
112+
return null;
113+
}
114+
115+
const svelteKitContextCache = createCache<Pick<
116+
SvelteContext,
117+
'svelteKitFileType' | 'svelteKitVersion'
118+
> | null>();
119+
99120
function getSvelteKitContext(
100121
context: RuleContext
101122
): Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> {
102123
const filePath = getFilename(context);
124+
125+
const cached = svelteKitContextCache.get(filePath);
126+
if (cached) return cached;
127+
103128
const svelteKitVersion = getSvelteKitVersion(filePath);
104129
if (svelteKitVersion == null) {
105-
return {
130+
const result: Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> = {
106131
svelteKitFileType: null,
107132
svelteKitVersion: null
108133
};
134+
svelteKitContextCache.set(filePath, result);
135+
return result;
109136
}
110137
if (isRunInBrowser) {
111-
return {
138+
const result: Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> = {
112139
svelteKitVersion,
113140
// Judge by only file path if it runs in browser.
114141
svelteKitFileType: getSvelteKitFileTypeFromFilePath(filePath)
115142
};
143+
svelteKitContextCache.set(filePath, result);
144+
return result;
116145
}
117146

118147
const routes =
@@ -123,21 +152,34 @@ function getSvelteKitContext(
123152
const projectRootDir = getProjectRootDir(getFilename(context)) ?? '';
124153

125154
if (!filePath.startsWith(path.join(projectRootDir, routes))) {
126-
return {
155+
const result: Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> = {
127156
svelteKitVersion,
128157
svelteKitFileType: null
129158
};
159+
svelteKitContextCache.set(filePath, result);
160+
return result;
130161
}
131162

132-
return {
163+
const result: Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> = {
133164
svelteKitVersion,
134165
svelteKitFileType: getSvelteKitFileTypeFromFilePath(filePath)
135166
};
167+
svelteKitContextCache.set(filePath, result);
168+
return result;
136169
}
137170

138-
function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
171+
const svelteVersionCache = createCache<SvelteContext['svelteVersion']>();
172+
173+
export function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
174+
const cached = svelteVersionCache.get(filePath);
175+
if (cached) return cached;
176+
139177
// Hack: if it runs in browser, it regards as Svelte project.
140-
if (isRunInBrowser) return '5';
178+
if (isRunInBrowser) {
179+
svelteVersionCache.set(filePath, '5');
180+
return '5';
181+
}
182+
141183
try {
142184
const packageJsons = getPackageJsons(filePath);
143185
for (const packageJson of packageJsons) {
@@ -147,17 +189,22 @@ function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
147189
}
148190
const major = extractMajorVersion(version, false);
149191
if (major === '3' || major === '4') {
192+
svelteVersionCache.set(filePath, '3/4');
150193
return '3/4';
151194
}
195+
svelteVersionCache.set(filePath, major as SvelteContext['svelteVersion']);
152196
return major as SvelteContext['svelteVersion'];
153197
}
154198
} catch {
155199
/** do nothing */
156200
}
157201

202+
svelteVersionCache.set(filePath, null);
158203
return null;
159204
}
160205

206+
const svelteKitVersionCache = createCache<SvelteContext['svelteKitVersion']>();
207+
161208
/**
162209
* Check givin file is under SvelteKit project.
163210
*
@@ -167,14 +214,22 @@ function getSvelteVersion(filePath: string): SvelteContext['svelteVersion'] {
167214
* @returns
168215
*/
169216
function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'] {
217+
const cached = svelteKitVersionCache.get(filePath);
218+
if (cached) return cached;
219+
170220
// Hack: if it runs in browser, it regards as SvelteKit project.
171-
if (isRunInBrowser) return '2';
221+
if (isRunInBrowser) {
222+
svelteKitVersionCache.set(filePath, '2');
223+
return '2';
224+
}
225+
172226
try {
173227
const packageJsons = getPackageJsons(filePath);
174228
if (packageJsons.length === 0) return null;
175229
if (packageJsons[0].name === 'eslint-plugin-svelte') {
176230
// Hack: CI removes `@sveltejs/kit` and it returns false and test failed.
177231
// So always it returns 2 if it runs on the package.
232+
svelteKitVersionCache.set(filePath, '2');
178233
return '2';
179234
}
180235

@@ -183,32 +238,22 @@ function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'
183238
packageJson.dependencies?.['@sveltejs/kit'] ??
184239
packageJson.devDependencies?.['@sveltejs/kit'];
185240
if (typeof version !== 'string') {
241+
svelteKitVersionCache.set(filePath, null);
186242
return null;
187243
}
188-
189-
return extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
244+
const major = extractMajorVersion(version, true) as SvelteContext['svelteKitVersion'];
245+
svelteKitVersionCache.set(filePath, major);
246+
return major;
190247
}
191248
} catch {
192249
/** do nothing */
193250
}
194251

252+
svelteKitVersionCache.set(filePath, null);
195253
return null;
196254
}
197255

198-
function extractMajorVersion(version: string, recognizePrereleaseVersion: boolean): string | null {
199-
if (recognizePrereleaseVersion) {
200-
const match = /^(?:\^|~)?(\d+\.0\.0-next)/.exec(version);
201-
if (match && match[1]) {
202-
return match[1];
203-
}
204-
}
205-
206-
const match = /^(?:\^|~)?(\d+)\./.exec(version);
207-
if (match && match[1]) {
208-
return match[1];
209-
}
210-
return null;
211-
}
256+
const projectRootDirCache = createCache<string | null>();
212257

213258
/**
214259
* Gets a project root folder path.
@@ -217,58 +262,81 @@ function extractMajorVersion(version: string, recognizePrereleaseVersion: boolea
217262
*/
218263
function getProjectRootDir(filePath: string): string | null {
219264
if (isRunInBrowser) return null;
265+
const cached = projectRootDirCache.get(filePath);
266+
if (cached) return cached;
267+
220268
const packageJsons = getPackageJsons(filePath);
221269
if (packageJsons.length === 0) {
270+
projectRootDirCache.set(filePath, null);
222271
return null;
223272
}
224273
const packageJsonFilePath = packageJsons[0].filePath;
225-
if (!packageJsonFilePath) return null;
226-
return path.dirname(path.resolve(packageJsonFilePath));
274+
if (!packageJsonFilePath) {
275+
projectRootDirCache.set(filePath, null);
276+
return null;
277+
}
278+
const projectRootDir = path.dirname(path.resolve(packageJsonFilePath));
279+
projectRootDirCache.set(filePath, projectRootDir);
280+
return projectRootDir;
227281
}
228282

283+
const svelteContextCache = createCache<SvelteContext | null>();
284+
229285
export function getSvelteContext(context: RuleContext): SvelteContext | null {
230286
const { parserServices } = getSourceCode(context);
231287
const { svelteParseContext } = parserServices;
232288
const filePath = getFilename(context);
289+
290+
const cached = svelteContextCache.get(filePath);
291+
if (cached) return cached;
292+
233293
const svelteKitContext = getSvelteKitContext(context);
234294
const svelteVersion = getSvelteVersion(filePath);
235295
const svelteFileType = getSvelteFileType(filePath);
236296

237297
if (svelteVersion == null) {
238-
return {
298+
const result: SvelteContext = {
239299
svelteVersion: null,
240300
svelteFileType: null,
241301
runes: null,
242302
svelteKitVersion: svelteKitContext.svelteKitVersion,
243303
svelteKitFileType: svelteKitContext.svelteKitFileType
244304
};
305+
svelteContextCache.set(filePath, result);
306+
return result;
245307
}
246308

247309
if (svelteVersion === '3/4') {
248-
return {
310+
const result: SvelteContext = {
249311
svelteVersion,
250312
svelteFileType: svelteFileType === '.svelte' ? '.svelte' : null,
251313
runes: null,
252314
svelteKitVersion: svelteKitContext.svelteKitVersion,
253315
svelteKitFileType: svelteKitContext.svelteKitFileType
254316
};
317+
svelteContextCache.set(filePath, result);
318+
return result;
255319
}
256320

257321
if (svelteFileType == null) {
258-
return {
322+
const result: SvelteContext = {
259323
svelteVersion,
260324
svelteFileType: null,
261325
runes: null,
262326
svelteKitVersion: svelteKitContext.svelteKitVersion,
263327
svelteKitFileType: svelteKitContext.svelteKitFileType
264328
};
329+
svelteContextCache.set(filePath, result);
330+
return result;
265331
}
266332

267-
return {
333+
const result: SvelteContext = {
268334
svelteVersion,
269335
runes: svelteParseContext?.runes ?? 'undetermined',
270336
svelteFileType,
271337
svelteKitVersion: svelteKitContext.svelteKitVersion,
272338
svelteKitFileType: svelteKitContext.svelteKitFileType
273339
};
340+
svelteContextCache.set(filePath, result);
341+
return result;
274342
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
export let children;
3+
</script>
4+
5+
{children}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"languageOptions": {
3+
"parserOptions": {
4+
"svelteConfig": {
5+
"kit": {
6+
"files": {
7+
"routes": "tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte4-children"
8+
}
9+
}
10+
}
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: disallow props other than data or errors in SvelteKit page components.
2+
line: 2
3+
column: 13
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": "^3.0.0 || ^4.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
export let data;
3+
export let errors;
4+
export let form;
5+
export let children;
6+
7+
let comment = '';
8+
9+
export const snapshot = {
10+
capture: () => comment,
11+
restore: (value) => (comment = value)
12+
};
13+
</script>
14+
15+
{data}, {errors}
16+
17+
{#if form?.success}
18+
<p>Successfully logged in! Welcome back, {data.user.name}</p>
19+
{/if}
20+
21+
<form method="POST">
22+
<textarea bind:value={comment} />
23+
<button>Post comment</button>
24+
</form>

0 commit comments

Comments
 (0)