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: {