-
-
Notifications
You must be signed in to change notification settings - Fork 48
feat: add control for executing rules based on Svelte/SvelteKit context #980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
0b44be4
4ca476a
4fbe97d
cb70fea
910effd
ad3dfc3
f1a1cca
531911a
a36d935
bbfae0e
8a02534
0707e79
280ae67
afd422d
f686b4d
ae40057
4640ef5
d7b588f
0e28cc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'eslint-plugin-svelte': minor | ||
--- | ||
|
||
feat: Implement util to conditionally run lint based on Svelte version and SvelteKit routes etc |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,50 @@ | ||
import type { RuleModule, PartialRuleModule } from '../types.js'; | ||
import type { RuleModule, PartialRuleModule, PartialRuleMetaData, RuleContext } from '../types.js'; | ||
import { getSvelteContext, type SvelteContext } from '../utils/svelte-context.js'; | ||
|
||
function isNotSatisfies<T>(actual: T, expected?: T[]): boolean { | ||
if (expected == null || expected.length === 0) { | ||
return false; | ||
} | ||
|
||
return !expected.includes(actual); | ||
} | ||
|
||
function satisfiesCondition( | ||
condition: NonNullable<PartialRuleMetaData['conditions']>[number], | ||
baseballyama marked this conversation as resolved.
Show resolved
Hide resolved
|
||
svelteContext: SvelteContext | ||
): boolean { | ||
if ( | ||
isNotSatisfies(svelteContext.svelteVersion, condition.svelteVersions) || | ||
isNotSatisfies(svelteContext.fileType, condition.fileTypes) || | ||
isNotSatisfies(svelteContext.runes, condition.runes) || | ||
isNotSatisfies(svelteContext.svelteKitVersion, condition.svelteKitVersions) || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already changed it to array. How can I make "runes" even more plural?😅 |
||
isNotSatisfies(svelteContext.svelteKitFileType, condition.svelteKitFileTypes) | ||
) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
// export for testing | ||
export function shouldRun( | ||
svelteContext: SvelteContext | null, | ||
conditions: PartialRuleMetaData['conditions'] | ||
): boolean { | ||
// If svelteContext is null, it means the rule might be executed based on the analysis result of a different parser. | ||
// In this case, always execute the rule. | ||
if (svelteContext == null || conditions == null || conditions.length === 0) { | ||
return true; | ||
} | ||
|
||
for (const condition of conditions) { | ||
if (satisfiesCondition(condition, svelteContext)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Define the rule. | ||
|
@@ -16,6 +62,14 @@ export function createRule(ruleName: string, rule: PartialRuleModule): RuleModul | |
ruleName | ||
} | ||
}, | ||
create: rule.create as never | ||
create(context: RuleContext) { | ||
const { conditions } = rule.meta; | ||
baseballyama marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const svelteContext = getSvelteContext(context); | ||
if (!shouldRun(svelteContext, conditions)) { | ||
return {}; | ||
} | ||
|
||
return rule.create(context); | ||
} | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import type { RuleContext } from '../types.js'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { getPackageJson } from './get-package-json.js'; | ||
import { getFilename, getSourceCode } from './compat.js'; | ||
|
||
const isRunInBrowser = !fs.readFileSync; | ||
|
||
export type SvelteContext = { | ||
svelteVersion: '3/4' | '5'; | ||
fileType: '.svelte' | '.svelte.[js|ts]'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I don't like the design of having separate
I think the second solution is more work (and requires quite careful design), but ultimately serves us better, because rules are fundamentally going to be enabled based on the properties of the current context/file, not based on its type... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At here, it’s better to keep things simple and store only basic information. This is because the definition of "context" can change depending on the rules. For example, we might want to create a rule that prevents placing files like In short, since the definition of "context" depends on the rules, it’s better to just manage file types here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still not convinced. I think we should instead add something like a Does that make sense to you or am I being too convoluted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is one of specific example why "Is this a layout file?" and "Is this a server-only file" is not enough. This rule should check only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, maybe I'm wrong (I haven't used SvelteKit server-side yet), but #438 should also check Anyway, if my change is not accepted, then I would like to think a little bit more about how There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And yes, the list of "file role flags" would probably be longer and would need to expand over time and some rules would check multiple flags at once... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. In that case, we can add helper functions for common validation patterns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. We need to check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah, that's probably a good enough alternative. I was thinking about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rename to
No.
![]()
|
||
runes: boolean; | ||
svelteKitVersion: '1-next' | '1' | '2' | null; | ||
svelteKitFileType: | ||
| '+page.svelte' | ||
| '+page.js' | ||
| '+page.server.js' | ||
| '+error.svelte' | ||
| '+layout.svelte' | ||
| '+layout.js' | ||
| '+layout.server.js' | ||
| '+server.js' | ||
| null; | ||
}; | ||
|
||
function getFileType(filePath: string): SvelteContext['fileType'] | null { | ||
if (filePath.endsWith('.svelte')) { | ||
return '.svelte'; | ||
} | ||
|
||
if (filePath.endsWith('.svelte.js') || filePath.endsWith('.svelte.ts')) { | ||
return '.svelte.[js|ts]'; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
function getSvelteKitFileTypeFromFilePath(filePath: string): SvelteContext['svelteKitFileType'] { | ||
const fileName = filePath.split('/').pop(); | ||
switch (fileName) { | ||
case '+page.svelte': { | ||
return '+page.svelte'; | ||
} | ||
case '+page.js': | ||
case '+page.ts': { | ||
return '+page.js'; | ||
} | ||
case '+page.server.js': | ||
case '+page.server.ts': { | ||
return '+page.server.js'; | ||
} | ||
case '+error.svelte': { | ||
return '+error.svelte'; | ||
} | ||
case '+layout.svelte': { | ||
return '+layout.svelte'; | ||
} | ||
case '+layout.js': | ||
case '+layout.ts': { | ||
return '+layout.js'; | ||
} | ||
case '+layout.server.js': | ||
case '+layout.server.ts': { | ||
return '+layout.server.js'; | ||
} | ||
case '+server.js': | ||
case '+server.ts': { | ||
return '+server.js'; | ||
} | ||
default: { | ||
return null; | ||
} | ||
} | ||
} | ||
|
||
function getSvelteKitContext( | ||
context: RuleContext | ||
): Pick<SvelteContext, 'svelteKitFileType' | 'svelteKitVersion'> { | ||
const filePath = getFilename(context); | ||
const svelteKitVersion = getSvelteKitVersion(filePath); | ||
if (svelteKitVersion == null) { | ||
return { | ||
svelteKitFileType: null, | ||
svelteKitVersion: null | ||
}; | ||
} | ||
if (isRunInBrowser) { | ||
baseballyama marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return { | ||
svelteKitVersion, | ||
// Judge by only file path if it runs in browser. | ||
svelteKitFileType: getSvelteKitFileTypeFromFilePath(filePath) | ||
}; | ||
} | ||
|
||
const routes = | ||
( | ||
context.settings?.svelte?.kit?.files?.routes ?? | ||
getSourceCode(context).parserServices.svelteParseContext?.svelteConfig?.kit?.files?.routes | ||
)?.replace(/^\//, '') ?? 'src/routes'; | ||
const projectRootDir = getProjectRootDir(getFilename(context)) ?? ''; | ||
|
||
if (!filePath.startsWith(path.join(projectRootDir, routes))) { | ||
return { | ||
svelteKitVersion, | ||
svelteKitFileType: null | ||
}; | ||
} | ||
|
||
return { | ||
svelteKitVersion, | ||
svelteKitFileType: getSvelteKitFileTypeFromFilePath(filePath) | ||
}; | ||
} | ||
|
||
/** | ||
* Check givin file is under SvelteKit project. | ||
* | ||
* If it runs on browser, it always returns true. | ||
* | ||
* @param filePath A file path. | ||
* @returns | ||
*/ | ||
function getSvelteKitVersion(filePath: string): SvelteContext['svelteKitVersion'] { | ||
// Hack: if it runs in browser, it regards as SvelteKit project. | ||
if (isRunInBrowser) return '2'; | ||
try { | ||
const packageJson = getPackageJson(filePath); | ||
if (!packageJson) return null; | ||
if (packageJson.name === 'eslint-plugin-svelte') | ||
// Hack: CI removes `@sveltejs/kit` and it returns false and test failed. | ||
// So always it returns true if it runs on the package. | ||
return '2'; | ||
|
||
const version = | ||
packageJson.dependencies?.['@sveltejs/kit'] ?? packageJson.devDependencies?.['@sveltejs/kit']; | ||
if (typeof version !== 'string') { | ||
return null; | ||
} | ||
if (version.startsWith('1.0.0-next.')) { | ||
return '1-next'; | ||
} else if (version.startsWith('1.')) { | ||
return '1'; | ||
} else if (version.startsWith('2.')) { | ||
return '2'; | ||
} | ||
// If unknown version, it recognize as v2. | ||
return '2'; | ||
} catch { | ||
return null; | ||
} | ||
} | ||
|
||
function getSvelteVersion(compilerVersion: string): SvelteContext['svelteVersion'] { | ||
const version = parseInt(compilerVersion.split('.')[0], 10); | ||
if (version === 3 || version === 4) { | ||
return '3/4'; | ||
} | ||
return '5'; | ||
} | ||
|
||
/** | ||
* Gets a project root folder path. | ||
* @param filePath A file path to lookup. | ||
* @returns A found project root folder path or null. | ||
*/ | ||
function getProjectRootDir(filePath: string): string | null { | ||
if (isRunInBrowser) return null; | ||
const packageJsonFilePath = getPackageJson(filePath)?.filePath; | ||
if (!packageJsonFilePath) return null; | ||
return path.dirname(path.resolve(packageJsonFilePath)); | ||
} | ||
|
||
export function getSvelteContext(context: RuleContext): SvelteContext | null { | ||
const { parserServices } = getSourceCode(context); | ||
const { svelteParseContext } = parserServices; | ||
if (svelteParseContext === undefined) { | ||
return null; | ||
} | ||
|
||
const { compilerVersion } = svelteParseContext; | ||
if (compilerVersion === undefined) { | ||
return null; | ||
} | ||
|
||
const filePath = getFilename(context); | ||
const svelteKitContext = getSvelteKitContext(context); | ||
|
||
const runes = svelteParseContext.runes === true; | ||
const fileType = getFileType(filePath); | ||
if (fileType === null) { | ||
return null; | ||
} | ||
|
||
return { | ||
svelteVersion: getSvelteVersion(compilerVersion), | ||
runes, | ||
fileType, | ||
svelteKitVersion: svelteKitContext.svelteKitVersion, | ||
svelteKitFileType: svelteKitContext.svelteKitFileType | ||
}; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.