From 9659780ac3ff038fae5adcda7f2a6f3b7aa7dc32 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 23 Mar 2024 10:33:58 +0900 Subject: [PATCH 1/3] feat: add support for flat config --- README.md | 58 +++++++++++++- docs/.vuepress/categories.js | 6 +- docs/.vuepress/components/rules/index.js | 2 +- docs/rules/README.md | 20 ++++- docs/rules/enforce-style-type.md | 2 +- docs/rules/no-deprecated-deep-combinator.md | 2 +- docs/rules/no-parent-of-v-global.md | 2 +- docs/rules/no-parsing-error.md | 2 +- docs/rules/no-unused-keyframes.md | 2 +- docs/rules/no-unused-selector.md | 2 +- docs/rules/require-v-deep-argument.md | 2 +- docs/rules/require-v-global-argument.md | 2 +- docs/rules/require-v-slotted-argument.md | 2 +- docs/user-guide/README.md | 40 +++++++++- lib/configs/flat/all.ts | 9 +++ lib/configs/flat/base.ts | 18 +++++ lib/configs/flat/recommended.ts | 9 +++ lib/configs/flat/vue2-recommended.ts | 9 +++ lib/configs/recommended.ts | 2 +- lib/index.ts | 8 ++ lib/rules/enforce-style-type.ts | 2 +- lib/rules/no-parsing-error.ts | 2 +- lib/rules/no-unused-keyframes.ts | 2 +- lib/rules/no-unused-selector.ts | 2 +- lib/rules/require-scoped.ts | 2 +- lib/types.ts | 2 +- lib/utils/rules.ts | 6 +- package-lock.json | 14 ++-- package.json | 2 +- tests/lib/configs/recommended.ts | 83 +++++++++++++++++++++ tests/lib/test-lib/eslint-compat.ts | 4 +- tools/lib/categories.ts | 42 ++++++++--- tools/lib/load-configs.ts | 12 +-- tools/render-rules.ts | 12 ++- tools/update-docs.ts | 25 ++++--- tools/update-readme.ts | 22 ++++++ tools/update-rules.ts | 2 +- 37 files changed, 367 insertions(+), 68 deletions(-) create mode 100644 lib/configs/flat/all.ts create mode 100644 lib/configs/flat/base.ts create mode 100644 lib/configs/flat/recommended.ts create mode 100644 lib/configs/flat/vue2-recommended.ts create mode 100644 tests/lib/configs/recommended.ts diff --git a/README.md b/README.md index 6eefbfb7..67c98462 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ npm install --save-dev eslint eslint-plugin-vue-scoped-css vue-eslint-parser ``` > **Requirements** -> +> > - ESLint v6.0.0 and above > - Node.js v12.22.x, v14.17.x, v16.x and above @@ -49,8 +49,32 @@ npm install --save-dev eslint eslint-plugin-vue-scoped-css vue-eslint-parser ## Usage + -Create `.eslintrc.*` file to configure rules. See also: [http://eslint.org/docs/user-guide/configuring](http://eslint.org/docs/user-guide/configuring). +### New (ESLint>=v9) Config (Flat Config) + +Use `eslint.config.js` file to configure rules. See also: . + +Example **eslint.config.js**: + +```mjs +import eslintPluginVueScopedCSS from 'eslint-plugin-vue-scoped-css'; +export default [ + // add more generic rule sets here, such as: + // js.configs.recommended, + ...eslintPluginVueScopedCSS.configs['flat/recommended'], + { + rules: { + // override/add rules settings here, such as: + // 'vue-scoped-css/no-unused-selector': 'error' + } + } +]; +``` + +### Legacy Config (ESLint. Example **.eslintrc.js**: @@ -72,11 +96,21 @@ module.exports = { This plugin provides some predefined configs: +### New (ESLint>=v9) Config (Flat Config) + +- `*.configs['flat/base']` - Settings and rules to enable this plugin +- `*.configs['flat/recommended']` - `/base`, plus rules for better ways to help you avoid problems for Vue.js 3.x +- `*.configs['flat/vue2-recommended']` - `/base`, plus rules for better ways to help you avoid problems for Vue.js 2.x +- `*.configs['flat/all']` - All rules of this plugin are included + +### Legacy Config (ESLint ## Rules @@ -91,9 +125,17 @@ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/comm Enforce all the rules in this category with: +```js +export default [ + ...eslintPluginVueScopedCSS.configs['flat/recommended'], +] +``` + +or + ```json { - "extends": "plugin:vue-scoped-css/vue3-recommended" + "extends": ["plugin:vue-scoped-css/vue3-recommended"] } ``` @@ -113,9 +155,17 @@ Enforce all the rules in this category with: Enforce all the rules in this category with: +```js +export default [ + ...eslintPluginVueScopedCSS.configs['flat/vue2-recommended'], +] +``` + +or + ```json { - "extends": "plugin:vue-scoped-css/recommended" + "extends": ["plugin:vue-scoped-css/recommended"] } ``` diff --git a/docs/.vuepress/categories.js b/docs/.vuepress/categories.js index 2c52ac26..96fa9430 100644 --- a/docs/.vuepress/categories.js +++ b/docs/.vuepress/categories.js @@ -14,18 +14,18 @@ const isCategoryTest = { recommended: ({ deprecated, docs: { categories } }) => !deprecated && categories.length && - categories.some((cat) => cat === "recommended") && + categories.some((cat) => cat === "vue2-recommended") && categories.some((cat) => cat === "vue3-recommended"), "vue2-recommended": ({ deprecated, docs: { categories } }) => !deprecated && categories.length && - categories.some((cat) => cat === "recommended") && + categories.some((cat) => cat === "vue2-recommended") && categories.every((cat) => cat !== "vue3-recommended"), "vue3-recommended": ({ deprecated, docs: { categories } }) => !deprecated && categories.length && categories.some((cat) => cat === "vue3-recommended") && - categories.every((cat) => cat !== "recommended"), + categories.every((cat) => cat !== "vue2-recommended"), uncategorized: ({ deprecated, docs: { categories } }) => !deprecated && !categories.length, deprecated: ({ deprecated }) => deprecated, diff --git a/docs/.vuepress/components/rules/index.js b/docs/.vuepress/components/rules/index.js index 06130ac9..7556f309 100644 --- a/docs/.vuepress/components/rules/index.js +++ b/docs/.vuepress/components/rules/index.js @@ -42,7 +42,7 @@ function getCategory({ deprecated, docs: { categories } }) { if (deprecated) { return "deprecated"; } - const v2 = categories.some((cat) => cat === "recommended"); + const v2 = categories.some((cat) => cat === "vue2-recommended"); const v3 = categories.some((cat) => cat === "vue3-recommended"); if (v2) { return v3 ? "recommended" : "vue2-recommended"; diff --git a/docs/rules/README.md b/docs/rules/README.md index c26d3c4f..ef66f93c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -10,9 +10,17 @@ sidebarDepth: 0 Enforce all the rules in this category with: +```js +export default [ + ...eslintPluginVueScopedCSS.configs['flat/recommended'], +] +``` + +or + ```json { - "extends": "plugin:vue-scoped-css/vue3-recommended" + "extends": ["plugin:vue-scoped-css/vue3-recommended"] } ``` @@ -32,9 +40,17 @@ Enforce all the rules in this category with: Enforce all the rules in this category with: +```js +export default [ + ...eslintPluginVueScopedCSS.configs['flat/vue2-recommended'], +] +``` + +or + ```json { - "extends": "plugin:vue-scoped-css/recommended" + "extends": ["plugin:vue-scoped-css/recommended"] } ``` diff --git a/docs/rules/enforce-style-type.md b/docs/rules/enforce-style-type.md index e24accca..24446a9a 100644 --- a/docs/rules/enforce-style-type.md +++ b/docs/rules/enforce-style-type.md @@ -8,7 +8,7 @@ description: "enforce the ``; +describe("`recommended` config", () => { + it("legacy `recommended` config should work. ", async () => { + const linter = new LegacyESLint({ + plugins: { + toml: plugin as never, + }, + baseConfig: { + parserOptions: { + ecmaVersion: 2020, + }, + extends: ["plugin:vue-scoped-css/vue3-recommended"], + }, + useEslintrc: false, + }); + const result = await linter.lintText(code, { filePath: "test.vue" }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ + ruleId: m.ruleId, + line: m.line, + message: m.message, + })), + [ + { + message: "Missing attribute `scoped`.", + ruleId: "vue-scoped-css/enforce-style-type", + line: 1, + }, + ], + ); + }); + it("`flat/recommended` config should work. ", async () => { + const linter = new ESLint({ + // @ts-expect-error -- typing bug + overrideConfigFile: true, + // @ts-expect-error -- typing bug + overrideConfig: [...plugin.configs["flat/recommended"]], + }); + const result = await linter.lintText(code, { filePath: "test.vue" }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ + ruleId: m.ruleId, + line: m.line, + message: m.message, + })), + [ + { + message: "Missing attribute `scoped`.", + ruleId: "vue-scoped-css/enforce-style-type", + line: 1, + }, + ], + ); + }); + it("`flat/recommended` config with *.js should work. ", async () => { + const linter = new ESLint({ + // @ts-expect-error -- typing bug + overrideConfigFile: true, + // @ts-expect-error -- typing bug + overrideConfig: [...plugin.configs["flat/recommended"]], + }); + + const result = await linter.lintText(";", { filePath: "test.js" }); + const messages = result[0].messages; + + assert.deepStrictEqual( + messages.map((m) => ({ + ruleId: m.ruleId, + line: m.line, + message: m.message, + })), + [], + ); + }); +}); diff --git a/tests/lib/test-lib/eslint-compat.ts b/tests/lib/test-lib/eslint-compat.ts index 6e541a36..3397aa13 100644 --- a/tests/lib/test-lib/eslint-compat.ts +++ b/tests/lib/test-lib/eslint-compat.ts @@ -2,7 +2,7 @@ import { getRuleTester, getRuleIdPrefix, } from "eslint-compat-utils/rule-tester"; -import { getLegacyESLint } from "eslint-compat-utils/eslint"; +import { getLegacyESLint, getESLint } from "eslint-compat-utils/eslint"; // eslint-disable-next-line @typescript-eslint/naming-convention -- Class name export const RuleTester = getRuleTester(); @@ -10,3 +10,5 @@ export const testRuleIdPrefix = getRuleIdPrefix(); // eslint-disable-next-line @typescript-eslint/naming-convention -- Class name export const LegacyESLint = getLegacyESLint(); +// eslint-disable-next-line @typescript-eslint/naming-convention -- Class name +export const ESLint = getESLint(); diff --git a/tools/lib/categories.ts b/tools/lib/categories.ts index 810fa7e0..60790cd7 100644 --- a/tools/lib/categories.ts +++ b/tools/lib/categories.ts @@ -1,22 +1,43 @@ import { rules } from "../../lib/utils/rules"; import type { Rule } from "../../lib/types"; +export const FLAT_PRESETS = { + "vue2-recommended": "flat/vue2-recommended", + "vue3-recommended": "flat/recommended", + base: "flat/base", + uncategorized: null, +}; +export const LEGACY_PRESETS = { + "vue2-recommended": "plugin:vue-scoped-css/recommended", + "vue3-recommended": "plugin:vue-scoped-css/vue3-recommended", + base: "plugin:vue-scoped-css/base", + uncategorized: null, +}; + const categoryTitles = { base: "Base Rules (Enabling Plugin)", "vue3-recommended": "Recommended for Vue.js 3.x", - recommended: "Recommended for Vue.js 2.x", -} as { [key: string]: string }; + "vue2-recommended": "Recommended for Vue.js 2.x", + uncategorized: undefined, +}; const categoryConfigDescriptions = { base: "Enable this plugin using with:", "vue3-recommended": "Enforce all the rules in this category with:", - recommended: "Enforce all the rules in this category with:", -} as { [key: string]: string }; + "vue2-recommended": "Enforce all the rules in this category with:", + uncategorized: undefined, +}; + +type CategoryId = keyof typeof categoryTitles; -const categoryIds = Object.keys(categoryTitles); -const categoryRules: { [key: string]: Rule[] } = rules.reduce( +const categoryIds: CategoryId[] = [ + "base", + "vue3-recommended", + "vue2-recommended", +]; +const categoryRules: Record = rules.reduce( (obj, rule) => { - const categoryNames = rule.meta.docs.categories.length + const categoryNames: CategoryId[] = rule.meta.docs.categories.length ? rule.meta.docs.categories : ["uncategorized"]; for (const cat of categoryNames) { @@ -25,12 +46,15 @@ const categoryRules: { [key: string]: Rule[] } = rules.reduce( } return obj; }, - {} as { [key: string]: Rule[] }, + {} as Record, ); // Throw if no title is defined for a category for (const categoryId of Object.keys(categoryRules)) { - if (categoryId !== "uncategorized" && !categoryTitles[categoryId]) { + if ( + categoryId !== "uncategorized" && + !categoryTitles[categoryId as CategoryId] + ) { throw new Error(`Category "${categoryId}" does not have a title defined.`); } } diff --git a/tools/lib/load-configs.ts b/tools/lib/load-configs.ts index 4383a9f1..1c31ff78 100644 --- a/tools/lib/load-configs.ts +++ b/tools/lib/load-configs.ts @@ -5,8 +5,7 @@ import { isDefined } from "../../lib/utils/utils"; type Config = { name: string; configId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- tools - config: any; + config: { rules?: Record; extends?: string | string[] }; path: string; extends: Config[]; }; @@ -17,12 +16,13 @@ type Config = { */ function readConfigs(): Config[] { const configsRoot = path.resolve(__dirname, "../../lib/configs"); - const result = fs.readdirSync(configsRoot); + const result = fs.readdirSync(configsRoot, { withFileTypes: true }); const configs = []; - for (const name of result) { - const configName = name.replace(/\.ts$/u, ""); + for (const dirent of result) { + if (!dirent.isFile()) continue; + const configName = dirent.name.replace(/\.ts$/u, ""); const configId = `plugin:vue-scoped-css/${configName}`; - const configPath = require.resolve(path.join(configsRoot, name)); + const configPath = require.resolve(path.join(configsRoot, dirent.name)); const config = require(configPath); configs.push({ diff --git a/tools/render-rules.ts b/tools/render-rules.ts index ccc037e0..d05127e6 100644 --- a/tools/render-rules.ts +++ b/tools/render-rules.ts @@ -1,4 +1,4 @@ -import categories from "./lib/categories"; +import categories, { FLAT_PRESETS, LEGACY_PRESETS } from "./lib/categories"; import type { Rule } from "../lib/types"; import { rules } from "../lib/utils/rules"; @@ -48,9 +48,17 @@ export default function renderRulesTableContent( ${category.configDescription} +\`\`\`js +export default [ + ...eslintPluginVueScopedCSS.configs['${FLAT_PRESETS[category.categoryId]}'], +] +\`\`\` + +or + \`\`\`json { - "extends": "plugin:vue-scoped-css/${category.categoryId}" + "extends": [${JSON.stringify(LEGACY_PRESETS[category.categoryId])}] } \`\`\` diff --git a/tools/update-docs.ts b/tools/update-docs.ts index 36a82fab..1a614e71 100644 --- a/tools/update-docs.ts +++ b/tools/update-docs.ts @@ -15,20 +15,25 @@ function formatItems(items: string[]) { } //eslint-disable-next-line require-jsdoc -- tools -function getPresets(categories: string[]) { - const categoryConfigs = configs.filter((conf) => - categories.includes(conf.name), +function getPresets(ruleId: string) { + const categoryConfigs = configs.filter( + (conf) => conf.config?.rules?.[ruleId] != null, ); if (!categoryConfigs.length) { return []; } const presets = new Set(categoryConfigs.map((cat) => cat.configId)); - const subTargets = configs.filter((conf) => - conf.extends.find((ext) => categories.includes(ext.name)), - ); - for (const name of getPresets(subTargets.map((s) => s.name))) { - presets.add(name); + for (;;) { + const extendsPreset = configs.filter( + (conf) => + !presets.has(conf.configId) && + [conf.config?.extends].flat().some((e) => e && presets.has(e)), + ); + if (!extendsPreset.length) break; + for (const e of extendsPreset) { + presets.add(e.configId); + } } return [...presets]; } @@ -65,7 +70,7 @@ class DocFile { meta: { fixable, deprecated, - docs: { ruleId, description, categories, replacedBy }, + docs: { ruleId, description, replacedBy }, }, } = this.rule; const title = `# ${ruleId}\n\n> ${description}`; @@ -86,7 +91,7 @@ class DocFile { } } else { const presets = Array.from( - new Set(getPresets(categories).concat(["plugin:vue-scoped-css/all"])), + new Set(getPresets(ruleId!).concat(["plugin:vue-scoped-css/all"])), ); if (presets.length) { diff --git a/tools/update-readme.ts b/tools/update-readme.ts index 9bce1c69..b6d7d1a4 100644 --- a/tools/update-readme.ts +++ b/tools/update-readme.ts @@ -45,3 +45,25 @@ fs.writeFileSync( ) .replace(/\n{3,}/gu, "\n\n"), ); + +const docsUserGuideFilePath = path.resolve( + __dirname, + "../docs/user-guide/README.md", +); +const docsUserGuide = fs.readFileSync(docsUserGuideFilePath, "utf8"); + +fs.writeFileSync( + docsUserGuideFilePath, + docsUserGuide + .replace( + /[\s\S]*/u, + /[\s\S]*/u.exec( + newReadme, + )![0], + ) + .replace( + /\(https:\/\/ota-meshi.github.io\/eslint-plugin-json-schema-validator(.*?)\)/gu, + (_s, c: string) => `(..${c.endsWith("/") ? `${c}README.md` : c})`, + ), + "utf8", +); diff --git a/tools/update-rules.ts b/tools/update-rules.ts index cbf0f8b4..541c8190 100644 --- a/tools/update-rules.ts +++ b/tools/update-rules.ts @@ -34,7 +34,7 @@ export const rules = baseRules.map(obj => { * @returns {Array} rules */ export function collectRules( - category?: "recommended" | "vue3-recommended", + category?: "vue2-recommended" | "vue3-recommended", ): { [key: string]: string } { return rules.reduce((obj, rule) => { if ( From 19c9656441535b00654333671fc8d6e60f9cd05d Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 23 Mar 2024 10:34:23 +0900 Subject: [PATCH 2/3] Create clean-wasps-bake.md --- .changeset/clean-wasps-bake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-wasps-bake.md diff --git a/.changeset/clean-wasps-bake.md b/.changeset/clean-wasps-bake.md new file mode 100644 index 00000000..42c82c3e --- /dev/null +++ b/.changeset/clean-wasps-bake.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-vue-scoped-css": minor +--- + +feat: add support for flat config From b6a11f4532a8ebc1dc316af3bfe4570ddc9332b5 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 23 Mar 2024 10:50:20 +0900 Subject: [PATCH 3/3] fix config --- lib/configs/all.ts | 7 +++++-- lib/configs/recommended.ts | 7 +++++-- lib/configs/vue3-recommended.ts | 7 +++++-- tests/lib/configs/recommended.ts | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/configs/all.ts b/lib/configs/all.ts index 0afe7a19..9edadeca 100644 --- a/lib/configs/all.ts +++ b/lib/configs/all.ts @@ -1,6 +1,9 @@ import { collectRules } from "../utils/rules"; - +import path from "path"; +const base = require.resolve("./base"); +const baseExtend = + path.extname(`${base}`) === ".ts" ? "plugin:vue-scoped-css/base" : base; export = { - extends: require.resolve("./base"), + extends: baseExtend, rules: collectRules(), }; diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts index a2422aec..653d62ee 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -1,6 +1,9 @@ import { collectRules } from "../utils/rules"; - +import path from "path"; +const base = require.resolve("./base"); +const baseExtend = + path.extname(`${base}`) === ".ts" ? "plugin:vue-scoped-css/base" : base; export = { - extends: require.resolve("./base"), + extends: baseExtend, rules: collectRules("vue2-recommended"), }; diff --git a/lib/configs/vue3-recommended.ts b/lib/configs/vue3-recommended.ts index a96414de..d320bc0c 100644 --- a/lib/configs/vue3-recommended.ts +++ b/lib/configs/vue3-recommended.ts @@ -1,6 +1,9 @@ import { collectRules } from "../utils/rules"; - +import path from "path"; +const base = require.resolve("./base"); +const baseExtend = + path.extname(`${base}`) === ".ts" ? "plugin:vue-scoped-css/base" : base; export = { - extends: require.resolve("./base"), + extends: baseExtend, rules: collectRules("vue3-recommended"), }; diff --git a/tests/lib/configs/recommended.ts b/tests/lib/configs/recommended.ts index d38bd3bc..4a080abb 100644 --- a/tests/lib/configs/recommended.ts +++ b/tests/lib/configs/recommended.ts @@ -7,7 +7,7 @@ describe("`recommended` config", () => { it("legacy `recommended` config should work. ", async () => { const linter = new LegacyESLint({ plugins: { - toml: plugin as never, + "vue-scoped-css": plugin as never, }, baseConfig: { parserOptions: {