From 088f0d33e44ef3a75ac04818de5f6252d6a86a06 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sun, 17 Mar 2024 18:07:46 +0900 Subject: [PATCH 1/4] feat: add support for flat config --- .eslintrc.js | 6 ++ README.md | 32 +++++++++ docs/user-guide.md | 32 +++++++++ src/configs/flat/all.ts | 18 +++++ src/configs/flat/base.ts | 30 ++++++++ src/configs/flat/prettier.ts | 23 ++++++ src/configs/flat/recommended.ts | 26 +++++++ src/index.ts | 10 ++- tests/src/configs/all.ts | 29 +++++++- tools/update-rulesets.ts | 121 +++++++++++++++++++++++++++++--- 10 files changed, 315 insertions(+), 12 deletions(-) create mode 100644 src/configs/flat/all.ts create mode 100644 src/configs/flat/base.ts create mode 100644 src/configs/flat/prettier.ts create mode 100644 src/configs/flat/recommended.ts diff --git a/.eslintrc.js b/.eslintrc.js index 1cda82569..3beeee000 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -59,6 +59,12 @@ module.exports = { 'mdx/code-blocks': true } }, + { + files: ['*.md/**', '**/*.md/**'], + rules: { + 'n/no-missing-import': 'off' + } + }, { files: ['*.mjs'], parserOptions: { diff --git a/README.md b/README.md index ebd15a809..4157b29f8 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,38 @@ npm install --save-dev eslint eslint-plugin-svelte svelte ### Configuration +#### For ESLint>=v9 Config (Flat Config) + +Use `eslint.config.js` file to configure rules. See also: . + +Example **eslint.config.js**: + +```mjs +import eslintPluginSvelte from 'eslint-plugin-svelte'; +export default [ + // add more generic rule sets here, such as: + // js.configs.recommended, + ...eslintPluginSvelte.configs['flat/recommended'], + { + rules: { + // override/add rules settings here, such as: + // 'svelte/rule-name': 'error' + } + } +]; +``` + +This plugin provides configs: + +- `eslintPluginSvelte.configs['flat/base']` ... Configuration to enable correct Svelte parsing. +- `eslintPluginSvelte.configs['flat/recommended']` ... Above, plus rules to prevent errors or unintended behavior. +- `eslintPluginSvelte.configs['flat/prettier']` ... Turns off rules that may conflict with [Prettier](https://prettier.io/) (You still need to configure prettier to work with svelte yourself, for example by using [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte).). +- `eslintPluginSvelte.configs['flat/all']` ... All rules. This configuration is not recommended for production use because it changes with every minor and major version of `eslint-plugin-svelte`. Use it at your own risk. + +See [the rule list](https://sveltejs.github.io/eslint-plugin-svelte/rules/) to get the `rules` that this plugin provides. + +#### Legacy Config (ESLint. Example **.eslintrc.js**: diff --git a/docs/user-guide.md b/docs/user-guide.md index ab76c64a9..099fe2bce 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -19,6 +19,38 @@ npm install --save-dev eslint eslint-plugin-svelte svelte ### Configuration +#### For ESLint>=v9 Config (Flat Config) + +Use `eslint.config.js` file to configure rules. See also: . + +Example **eslint.config.js**: + +```mjs +import eslintPluginSvelte from 'eslint-plugin-svelte'; +export default [ + // add more generic rule sets here, such as: + // js.configs.recommended, + ...eslintPluginSvelte.configs['flat/recommended'], + { + rules: { + // override/add rules settings here, such as: + // 'svelte/rule-name': 'error' + } + } +]; +``` + +This plugin provides configs: + +- `eslintPluginSvelte.configs['flat/base']` ... Configuration to enable correct Svelte parsing. +- `eslintPluginSvelte.configs['flat/recommended']` ... Above, plus rules to prevent errors or unintended behavior. +- `eslintPluginSvelte.configs['flat/prettier']` ... Turns off rules that may conflict with [Prettier](https://prettier.io/) (You still need to configure prettier to work with svelte yourself, for example by using [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte).). +- `eslintPluginSvelte.configs['flat/all']` ... All rules. This configuration is not recommended for production use because it changes with every minor and major version of `eslint-plugin-svelte`. Use it at your own risk. + +See [the rule list](./rules.md) to get the `rules` that this plugin provides. + +#### Legacy Config (ESLint. Example **.eslintrc.js**: diff --git a/src/configs/flat/all.ts b/src/configs/flat/all.ts new file mode 100644 index 000000000..c2dfe9771 --- /dev/null +++ b/src/configs/flat/all.ts @@ -0,0 +1,18 @@ +import { rules } from '../../utils/rules'; +import base from './base'; +export default [ + ...base, + { + rules: Object.fromEntries( + rules + .map((rule) => [`svelte/${rule.meta.docs.ruleName}`, 'error']) + .filter( + ([ruleName]) => + ![ + // Does not work without options. + 'svelte/no-restricted-html-elements' + ].includes(ruleName) + ) + ) + } +]; diff --git a/src/configs/flat/base.ts b/src/configs/flat/base.ts new file mode 100644 index 000000000..54d4b2638 --- /dev/null +++ b/src/configs/flat/base.ts @@ -0,0 +1,30 @@ +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" +import type { ESLint } from 'eslint'; +export default [ + { + files: ['*.svelte', '**/*.svelte'], + plugins: { + get svelte(): ESLint.Plugin { + // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore + return require('../../index'); + } + }, + languageOptions: { + // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore + parser: require('svelte-eslint-parser') + }, + rules: { + // ESLint core rules known to cause problems with `.svelte`. + 'no-inner-declarations': 'off', // The AST generated by svelte-eslint-parser will false positives in it rule because the root node of the script is not the `Program`. + // "no-irregular-whitespace": "off", + // Self assign is one of way to update reactive value in Svelte. + 'no-self-assign': 'off', + + // eslint-plugin-svelte rules + 'svelte/comment-directive': 'error', + 'svelte/system': 'error' + } + } +]; diff --git a/src/configs/flat/prettier.ts b/src/configs/flat/prettier.ts new file mode 100644 index 000000000..5b2b1543b --- /dev/null +++ b/src/configs/flat/prettier.ts @@ -0,0 +1,23 @@ +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" +import base from './base'; +export default [ + ...base, + { + rules: { + // eslint-plugin-svelte rules + 'svelte/first-attribute-linebreak': 'off', + 'svelte/html-closing-bracket-spacing': 'off', + 'svelte/html-quotes': 'off', + 'svelte/html-self-closing': 'off', + 'svelte/indent': 'off', + 'svelte/max-attributes-per-line': 'off', + 'svelte/mustache-spacing': 'off', + 'svelte/no-spaces-around-equal-signs-in-attribute': 'off', + 'svelte/no-trailing-spaces': 'off', + 'svelte/shorthand-attribute': 'off', + 'svelte/shorthand-directive': 'off' + } + } +]; diff --git a/src/configs/flat/recommended.ts b/src/configs/flat/recommended.ts new file mode 100644 index 000000000..7e2c4a80d --- /dev/null +++ b/src/configs/flat/recommended.ts @@ -0,0 +1,26 @@ +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" +import base from './base'; +export default [ + ...base, + { + rules: { + // eslint-plugin-svelte rules + 'svelte/comment-directive': 'error', + 'svelte/no-at-debug-tags': 'warn', + 'svelte/no-at-html-tags': 'error', + 'svelte/no-dupe-else-if-blocks': 'error', + 'svelte/no-dupe-style-properties': 'error', + 'svelte/no-dynamic-slot-name': 'error', + 'svelte/no-inner-declarations': 'error', + 'svelte/no-not-function-handler': 'error', + 'svelte/no-object-in-text-mustaches': 'error', + 'svelte/no-shorthand-style-property-overrides': 'error', + 'svelte/no-unknown-style-directive-property': 'error', + 'svelte/no-unused-svelte-ignore': 'error', + 'svelte/system': 'error', + 'svelte/valid-compile': 'error' + } + } +]; diff --git a/src/index.ts b/src/index.ts index ed6162c8c..8aed92235 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,10 @@ import base from './configs/base'; import recommended from './configs/recommended'; import prettier from './configs/prettier'; import all from './configs/all'; +import flatBase from './configs/flat/base'; +import flatRecommended from './configs/flat/recommended'; +import flatPrettier from './configs/flat/prettier'; +import flatAll from './configs/flat/all'; import * as processor from './processor'; import * as meta from './meta'; @@ -11,7 +15,11 @@ const configs = { base, recommended, prettier, - all + all, + 'flat/base': flatBase, + 'flat/recommended': flatRecommended, + 'flat/prettier': flatPrettier, + 'flat/all': flatAll }; const rules = ruleList.reduce( diff --git a/tests/src/configs/all.ts b/tests/src/configs/all.ts index cf67a89a1..3642b539c 100644 --- a/tests/src/configs/all.ts +++ b/tests/src/configs/all.ts @@ -1,9 +1,9 @@ import assert from 'assert'; import plugin from '../../../src/index'; -import { LegacyESLint } from '../../utils/eslint-compat'; +import { LegacyESLint, ESLint } from '../../utils/eslint-compat'; describe('`all` config', () => { - it('`all` config should work. ', async () => { + it('legacy `all` config should work. ', async () => { const code = `{@html a+b}`; const linter = new LegacyESLint({ @@ -22,10 +22,33 @@ describe('`all` config', () => { const messages = result[0].messages; assert.deepStrictEqual( - messages.map((m) => ({ ruleId: m.ruleId, line: m.line })), + messages.map((m) => ({ ruleId: m.ruleId, line: m.line, message: m.message })), + [ + { + ruleId: 'svelte/no-at-html-tags', + message: '`{@html}` can lead to XSS attack.', + line: 1 + } + ] + ); + }); + it('`all` config should work. ', async () => { + const code = `{@html a+b}`; + + const linter = new ESLint({ + overrideConfigFile: true as any, + // eslint-disable-next-line @typescript-eslint/no-var-requires -- for test + overrideConfig: require('../../../src/index').configs['flat/all'] + }); + const result = await linter.lintText(code, { filePath: 'test.svelte' }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ ruleId: m.ruleId, line: m.line, message: m.message })), [ { ruleId: 'svelte/no-at-html-tags', + message: '`{@html}` can lead to XSS attack.', line: 1 } ] diff --git a/tools/update-rulesets.ts b/tools/update-rulesets.ts index 8680b8b67..41c6537a8 100644 --- a/tools/update-rulesets.ts +++ b/tools/update-rulesets.ts @@ -2,7 +2,11 @@ import path from 'path'; import { rules } from './lib/load-rules'; import { writeAndFormat } from './lib/write'; -const baseContent = `/* +// ------------------ +// Legacy Config +// ------------------ + +const legacyBaseContent = `/* * IMPORTANT! * This file has been automatically generated, * in order to update its content execute "pnpm run update" @@ -34,12 +38,12 @@ export = { } `; -const baseFilePath = path.resolve(__dirname, '../src/configs/base.ts'); +const legacyBaseFilePath = path.resolve(__dirname, '../src/configs/base.ts'); // Update file. -void writeAndFormat(baseFilePath, baseContent); +void writeAndFormat(legacyBaseFilePath, legacyBaseContent); -const recommendedContent = `/* +const legacyRecommendedContent = `/* * IMPORTANT! * This file has been automatically generated, * in order to update its content execute "pnpm run update" @@ -63,12 +67,12 @@ export = { } `; -const recommendedFilePath = path.resolve(__dirname, '../src/configs/recommended.ts'); +const legacyRecommendedFilePath = path.resolve(__dirname, '../src/configs/recommended.ts'); // Update file. -void writeAndFormat(recommendedFilePath, recommendedContent); +void writeAndFormat(legacyRecommendedFilePath, legacyRecommendedContent); -const prettierContent = `/* +const legacyPrettierContent = `/* * IMPORTANT! * This file has been automatically generated, * in order to update its content execute "pnpm run update" @@ -89,7 +93,108 @@ export = { } `; -const prettierFilePath = path.resolve(__dirname, '../src/configs/prettier.ts'); +const legacyPrettierFilePath = path.resolve(__dirname, '../src/configs/prettier.ts'); + +// Update file. +void writeAndFormat(legacyPrettierFilePath, legacyPrettierContent); + +// ------------------ +// Flat Config +// ------------------ + +const baseContent = `/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ +import type { ESLint } from 'eslint'; +export default [ + { + files: ["*.svelte", "**/*.svelte"], + plugins: { + get svelte(): ESLint.Plugin { + // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore + return require("../../index") + } + }, + languageOptions: { + // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore + parser: require('svelte-eslint-parser'), + }, + rules: { + // ESLint core rules known to cause problems with \`.svelte\`. + "no-inner-declarations": "off", // The AST generated by svelte-eslint-parser will false positives in it rule because the root node of the script is not the \`Program\`. + // "no-irregular-whitespace": "off", + // Self assign is one of way to update reactive value in Svelte. + "no-self-assign": "off", + + // eslint-plugin-svelte rules + ${rules + .filter((rule) => rule.meta.docs.recommended === 'base' && !rule.meta.deprecated) + .map((rule) => { + const conf = rule.meta.docs.default || 'error'; + return `"${rule.meta.docs.ruleId}": "${conf}"`; + }) + .join(',\n ')}, + }, + }, +] +`; + +const baseFilePath = path.resolve(__dirname, '../src/configs/flat/base.ts'); + +// Update file. +void writeAndFormat(baseFilePath, baseContent); + +const recommendedContent = `/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ +import base from "./base" +export default [ + ...base, + { + rules: { + // eslint-plugin-svelte rules + ${rules + .filter((rule) => rule.meta.docs.recommended && !rule.meta.deprecated) + .map((rule) => { + const conf = rule.meta.docs.default || 'error'; + return `"${rule.meta.docs.ruleId}": "${conf}"`; + }) + .join(',\n ')}, + }, + } +] +`; + +const recommendedFilePath = path.resolve(__dirname, '../src/configs/flat/recommended.ts'); + +// Update file. +void writeAndFormat(recommendedFilePath, recommendedContent); + +const prettierContent = `/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ +import base from "./base" +export default [ + ...base, + { + rules: { + // eslint-plugin-svelte rules + ${rules + .filter((rule) => rule.meta.docs.conflictWithPrettier) + .map((rule) => `"${rule.meta.docs.ruleId}": "off"`) + .join(',\n ')}, + }, + } +] +`; + +const prettierFilePath = path.resolve(__dirname, '../src/configs/flat/prettier.ts'); // Update file. void writeAndFormat(prettierFilePath, prettierContent); From a75ef566894ba07fb48d9b43ff8c968ba7682f7d Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 17 Mar 2024 18:08:17 +0900 Subject: [PATCH 2/4] Create gentle-apricots-float.md --- .changeset/gentle-apricots-float.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gentle-apricots-float.md diff --git a/.changeset/gentle-apricots-float.md b/.changeset/gentle-apricots-float.md new file mode 100644 index 000000000..d2841c228 --- /dev/null +++ b/.changeset/gentle-apricots-float.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: add support for flat config From 0192b62586c0485f5fae2447ca613d1b4fa8f867 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sun, 17 Mar 2024 18:11:20 +0900 Subject: [PATCH 3/4] fix for eslint v7 --- tests/src/configs/all.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/configs/all.ts b/tests/src/configs/all.ts index 3642b539c..7744c2dad 100644 --- a/tests/src/configs/all.ts +++ b/tests/src/configs/all.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import semver from 'semver'; import plugin from '../../../src/index'; import { LegacyESLint, ESLint } from '../../utils/eslint-compat'; @@ -33,6 +34,7 @@ describe('`all` config', () => { ); }); it('`all` config should work. ', async () => { + if (semver.satisfies(ESLint.version, '<8.0.0')) return; const code = `{@html a+b}`; const linter = new ESLint({ From e3c0beb84d63dbc79691e6e46d1a21f7547a8dde Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Mon, 18 Mar 2024 08:25:40 +0900 Subject: [PATCH 4/4] update test --- tests/src/configs/all.ts | 5 ++- tests/src/configs/recommended.ts | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/src/configs/recommended.ts diff --git a/tests/src/configs/all.ts b/tests/src/configs/all.ts index 7744c2dad..40f3d3968 100644 --- a/tests/src/configs/all.ts +++ b/tests/src/configs/all.ts @@ -38,9 +38,8 @@ describe('`all` config', () => { const code = `{@html a+b}`; const linter = new ESLint({ - overrideConfigFile: true as any, - // eslint-disable-next-line @typescript-eslint/no-var-requires -- for test - overrideConfig: require('../../../src/index').configs['flat/all'] + overrideConfigFile: true as never, + overrideConfig: plugin.configs['flat/all'] as never }); const result = await linter.lintText(code, { filePath: 'test.svelte' }); const messages = result[0].messages; diff --git a/tests/src/configs/recommended.ts b/tests/src/configs/recommended.ts new file mode 100644 index 000000000..4610390b8 --- /dev/null +++ b/tests/src/configs/recommended.ts @@ -0,0 +1,58 @@ +import assert from 'assert'; +import semver from 'semver'; +import plugin from '../../../src/index'; +import { LegacyESLint, ESLint } from '../../utils/eslint-compat'; + +describe('`all` config', () => { + it('legacy `all` config should work. ', async () => { + const code = `{@html a+b}`; + + const linter = new LegacyESLint({ + plugins: { + svelte: plugin as never + }, + baseConfig: { + parserOptions: { + ecmaVersion: 2020 + }, + extends: ['plugin:svelte/recommended'] + }, + useEslintrc: false + }); + const result = await linter.lintText(code, { filePath: 'test.svelte' }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ ruleId: m.ruleId, line: m.line, message: m.message })), + [ + { + ruleId: 'svelte/no-at-html-tags', + message: '`{@html}` can lead to XSS attack.', + line: 1 + } + ] + ); + }); + it('`all` config should work. ', async () => { + if (semver.satisfies(ESLint.version, '<8.0.0')) return; + const code = `{@html a+b}`; + + const linter = new ESLint({ + overrideConfigFile: true as never, + overrideConfig: plugin.configs['flat/recommended'] as never + }); + const result = await linter.lintText(code, { filePath: 'test.svelte' }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ ruleId: m.ruleId, line: m.line, message: m.message })), + [ + { + ruleId: 'svelte/no-at-html-tags', + message: '`{@html}` can lead to XSS attack.', + line: 1 + } + ] + ); + }); +});