UNPKG

@kouts/eslint-config

Version:
297 lines (289 loc) 8.93 kB
import js from '@eslint/js'; import pluginHtml from 'eslint-plugin-html'; import pluginPrettier from 'eslint-plugin-prettier/recommended'; import pluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; import neostandard, { plugins, resolveIgnoresFromGitignore } from 'neostandard'; export { plugins } from 'neostandard'; import pluginVitest from '@vitest/eslint-plugin'; import pluginVue from 'eslint-plugin-vue'; const rule = { meta: { type: "suggestion", docs: { description: "Require defineOptions with name attribute in script setup Vue components", category: "Best Practices", recommended: true }, schema: [] }, create(context) { let hasDefineOptionsWithName = false; return { // We'll assume every Vue file with script setup needs a defineOptions name // Check for defineOptions function calls 'CallExpression[callee.name="defineOptions"]'(node) { if (node.arguments?.length && node.arguments[0]?.type === "ObjectExpression") { const properties = node.arguments[0].properties || []; for (const prop of properties) { if (prop.type === "Property" && prop.key?.type === "Identifier" && prop.key?.name === "name") { hasDefineOptionsWithName = true; break; } } } }, // At the end of the file, report if we don't have defineOptions with name "Program:exit"(node) { const filename = context.physicalFilename; const isVueFile = filename.endsWith(".vue"); if (isVueFile && !hasDefineOptionsWithName) { const sourceCode = context.sourceCode.getText(); const hasScriptSetup = /<script\s+setup/.test(sourceCode); if (hasScriptSetup) { context.report({ node, message: 'Component name is required. Add defineOptions({ name: "YourComponentName" }) to your <script setup>' }); } } } }; } }; var customRules = { "vue-require-name-in-setup": rule }; const typescript = [ ...plugins["typescript-eslint"].configs.recommended, { name: "kouts/typescript", rules: { // Prefer T[] instead of Array<T> "@typescript-eslint/array-type": ["error", { default: "array" }], // Prefer type over interface for objects "@typescript-eslint/consistent-type-definitions": ["error", "type"], // Fix type imports "@typescript-eslint/consistent-type-imports": [ "error", { prefer: "type-imports", fixStyle: "inline-type-imports" } ], // Allow _ for unused variables "@typescript-eslint/no-unused-vars": [ "error", { args: "none", caughtErrors: "none", ignoreRestSiblings: true, vars: "all", argsIgnorePattern: "^_" } ], // Disallow non-null assertions using the ! postfix operator "@typescript-eslint/no-non-null-assertion": "error", // Disallow enums "no-restricted-syntax": [ "error", { selector: "TSEnumDeclaration", message: `Enums introduce unexpected runtime behavior, break TypeScript's structural typing, and add unnecessary complexity. Use union types or 'as const' objects instead.` } ] } } ]; const vitest = [ { name: "kouts/vitest", files: ["test/**", "tests/**", "**/*.test.{js,ts}*", "**/*.spec.{js,ts}"], plugins: { vitest: pluginVitest }, rules: { ...pluginVitest.configs.recommended.rules, "vitest/expect-expect": [ "error", { assertFunctionNames: ["expect", "assert", "expectTypeOf"] } ] }, languageOptions: { globals: { ...pluginVitest.environments.env.globals } } } ]; const vue = (options) => { const opts = { version: 3, ts: true, ...options }; const vueConfig = opts.version === 3 ? pluginVue.configs["flat/recommended"] : pluginVue.configs["flat/vue2-recommended"]; const languageOptions = opts.ts ? { languageOptions: { parserOptions: { parser: plugins["typescript-eslint"].parser } } } : {}; const config = [ ...vueConfig, { name: "kouts/vue", files: ["*.vue", "**/*.vue"], ...languageOptions, rules: { // Custom rules "kouts/vue-require-name-in-setup": "error", // Overrides for vue/(vue3-)recommended preset "vue/max-attributes-per-line": "off", "vue/singleline-html-element-content-newline": "off", // Strengthen vue/(vue3-)recommended preset for autofix // https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/configs/recommended.js "vue/attributes-order": "error", "vue/block-order": "error", "vue/no-lone-template": "error", "vue/no-multiple-slot-args": "error", "vue/no-v-html": "error", "vue/order-in-components": "error", "vue/this-in-template": "error", "vue/require-prop-types": "error", // Enforce PascalCase for Vue components "vue/component-name-in-template-casing": ["error", "PascalCase", { registeredComponentsOnly: false, ignores: [] }], // Do not allow inline styles "vue/no-static-inline-styles": ["error", { allowBinding: false }], // Require explicit emits "vue/require-explicit-emits": "error", // Require component to have a name property "vue/require-name-property": "error", // Require components that don't have any content to self-close "vue/html-self-closing": [ "error", { html: { void: "always", normal: "never", component: "always" }, svg: "always", math: "always" } ], // Enforce dot notation whenever possible in `<template>` "vue/dot-notation": ["error"] } } ]; return config; }; const customRulesPlugin = { name: "kouts", rules: customRules }; const config = (options) => { const opts = { noJsx: true, noStyle: true, semi: false, ts: true, vue: true, vueVersion: 3, vitest: true, ...options }; const linterConfig = [ // Custom rules plugin { name: "kouts/custom-rules", plugins: { kouts: customRulesPlugin } }, { name: "kouts/ignores", ignores: [ "**/node_modules/**", "{tmp,temp}/**", "**/*.min.js", "vendor/**", "dist/**", "public/**", ...resolveIgnoresFromGitignore() ] }, // JavaScript { name: "kouts/javascript", ...js.configs.recommended }, // TypeScript ...opts.ts ? typescript : [], // Neostandard ...neostandard(opts), { name: "kouts/neostandard-overrides", rules: { // Enforce blank lines between the given 2 kinds of statements "padding-line-between-statements": [ "error", { blankLine: "always", prev: "*", next: "return" }, { blankLine: "always", prev: ["const", "let", "var"], next: "*" }, { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"] }, { blankLine: "always", prev: "directive", next: "*" }, { blankLine: "any", prev: "directive", next: "directive" } ], // Console and debugger settings depending whether we're on production or not "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", // Enforce dot notation "dot-notation": ["error"], // Import rules ...opts.ts ? { "import-x/named": "off", "no-unused-vars": "off" } : {}, "import-x/no-mutable-exports": "error", "import-x/newline-after-import": ["error", { count: 1 }], "import-x/no-self-import": "error" } }, // Sort imports { name: "kouts/sort-imports", plugins: { "simple-import-sort": pluginSimpleImportSort }, rules: { "simple-import-sort/imports": [ "error", // Remove all blank lines between imports { groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]] } ], "simple-import-sort/exports": "error" } }, // HTML { name: "kouts/html", files: ["**/*.html"], plugins: { html: pluginHtml } }, // Vue ...opts.vue ? vue({ version: opts.vueVersion, ts: opts.ts }) : [], // Vitest ...opts.vitest ? vitest : [], // Prettier { name: "kouts/prettier", ...pluginPrettier } ]; return linterConfig; }; export { config };