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
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: {
+ "vue-scoped-css": 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 (