Skip to content

Instantly share code, notes, and snippets.

@ShayanTheNerd
Last active May 24, 2024 20:35
Show Gist options
  • Save ShayanTheNerd/6c1ba67117b4bd9115fd48036316820b to your computer and use it in GitHub Desktop.
Save ShayanTheNerd/6c1ba67117b4bd9115fd48036316820b to your computer and use it in GitHub Desktop.
Exhaustive ESLint config for Vue.js, TypeScript, and Tailwind CSS
import jsESLint from '@eslint/js';
import vueESLint from 'eslint-plugin-vue';
import antfuConfig from '@antfu/eslint-config';
import htmlESLintParser from '@html-eslint/parser';
import htmlESLint from '@html-eslint/eslint-plugin';
import stylisticESLint from '@stylistic/eslint-plugin';
/* eslint-disable no-magic-numbers -- Improve SNR */
const jsRules = {
...jsESLint.configs.recommended.rules,
/* Possible Problems */
'no-await-in-loop': 'error',
'no-self-compare': 'error',
'no-unreachable-loop': 'error',
'no-inner-declarations': 'error',
'array-callback-return': 'error',
'no-useless-assignment': 'error',
'no-constructor-return': 'error',
'require-atomic-updates': 'error',
'no-async-promise-executor': 'error',
'no-template-curly-in-string': 'error',
'no-promise-executor-return': 'error',
'no-unmodified-loop-condition': 'error',
'no-use-before-define': ['error', 'nofunc'],
'use-isnan': ['error', { enforceForIndexOf: true }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-unsafe-negation': ['error', { enforceForOrderingRelations: true }],
'no-unsafe-optional-chaining': ['error', { disallowArithmeticOperators: true }],
/* Suggestions */
'yoda': 'error',
'strict': 'error',
'no-var': 'error',
'eqeqeq': 'error',
'no-new': 'error',
'no-eval': 'error',
'no-void': 'error',
'new-cap': 'error',
'no-proto': 'error',
'no-caller': 'error',
'no-empty': 'error',
'no-eq-null': 'error',
'camelcase': 'error',
'sort-imports': 'off',
'no-redeclare': 'off',
'complexity': 'error',
'no-plusplus': 'error',
'no-iterator': 'error',
'no-continue': 'error',
'no-lonely-if': 'error',
'max-params': 'error',
'no-shadow': ['error'],
'no-label-var': 'error',
'no-new-func': 'error',
'no-multi-str': 'error',
'guard-for-in': 'error',
'no-loop-func': 'error',
'default-case': 'error',
'no-script-url': 'error',
'prefer-const': 'error',
'no-undefined': 'error',
'no-undef-init': 'error',
'require-await': 'error',
'no-extra-bind': 'error',
'prefer-spread': 'error',
'no-lone-blocks': 'error',
'no-extra-label': 'error',
'no-invalid-this': 'error',
'accessor-pairs': 'error',
'no-useless-call': 'error',
'max-depth': ['error', 3],
'no-implied-eval': 'error',
'consistent-this': 'error',
'no-octal-escape': 'error',
'no-throw-literal': 'error',
'prefer-template': 'error',
'init-declarations': 'error',
'no-new-wrappers': 'error',
'block-scoped-var': 'error',
'no-extend-native': 'error',
'default-case-last': 'error',
'object-shorthand': 'error',
'no-useless-return': 'error',
'consistent-return': 'error',
'no-useless-concat': 'error',
'no-param-reassign': 'error',
'no-nested-ternary': 'error',
'no-useless-rename': 'error',
'no-implicit-globals': 'error',
'default-param-last': 'error',
'symbol-description': 'error',
'curly': ['error', 'multi-line'],
'prefer-rest-params': 'error',
'no-implicit-coercion': 'error',
'radix': ['error', 'as-needed'],
'func-name-matching': 'error',
'operator-assignment': 'error',
'no-unneeded-ternary': 'error',
'no-array-constructor': 'error',
'prefer-destructuring': 'error',
'no-underscore-dangle': 'error',
'prefer-object-spread': 'error',
'prefer-arrow-callback': 'error',
'no-object-constructor': 'error',
'prefer-object-has-own': 'error',
'no-useless-constructor': 'error',
'class-methods-use-this': 'error',
'require-unicode-regexp': 'error',
'prefer-numeric-literals': 'error',
'no-useless-computed-key': 'error',
'max-nested-callbacks': ['error', 3],
'no-return-assign': ['error', 'always'],
'prefer-named-capture-group': 'error',
'no-bitwise': ['error', { int32Hint: true }],
'prefer-exponentiation-operator': 'error',
'import/extensions': ['error', 'ignorePackages'],
'dot-notation': ['error', { allowKeywords: false }],
'logical-assignment-operators': ['error', 'always'],
'no-sequences': ['error', { allowInParentheses: false }],
'no-multi-assign': ['error', { ignoreNonDeclaration: true }],
'no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
'no-extra-boolean-cast': ['error', { enforceForLogicalOperands: true }],
'no-restricted-exports': ['error', { restrictedNamedExports: ['default'] }],
'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }],
'arrow-body-style': ['error', 'as-needed', { requireReturnForObjectLiteral: true }],
'no-magic-numbers': ['error', {
enforceConst: true,
detectObjects: true,
ignoreDefaultValues: true,
ignoreClassFieldInitialValues: true,
ignore: [-1, 0, 1, 100],
}],
};
const tsRules = {
'ts/no-misused-promises': 'off',
'ts/no-unsafe-assignment': 'off',
'ts/member-delimiter-style': 'error',
'ts/type-annotation-spacing': 'error',
'ts/consistent-type-definitions': ['error', 'type'],
};
const vueRules = {
...vueESLint.configs.recommended.rules,
/* Base */
'vue/jsx-uses-vars': 'error',
'vue/comment-directive': ['error', { reportUnusedDisableDirectives: true }],
/* Priority B: Strongly Recommended (Improving Readability) */
'vue/singleline-html-element-content-newline': 'off',
'vue/max-attributes-per-line': ['error', { singleline: 3 }],
'vue/v-slot-style': ['error', { atComponent: 'shorthand' }],
'vue/html-self-closing': ['error', { html: { void: 'always' } }],
'vue/first-attribute-linebreak': ['error', { singleline: 'beside' }],
'vue/html-indent': ['error', 'tab', { attribute: 1, baseIndent: 1 }],
'vue/v-on-event-hyphenation': ['error', 'always', { autofix: true }],
'vue/v-bind-style': ['error', 'shorthand', { sameNameShorthand: 'always' }],
/* Priority C: Recommended (Potentially Dangerous Patterns) */
'vue/attributes-order': ['error', {
order: [
'RENDER_MODIFIERS',
'DEFINITION',
'CONDITIONALS',
'LIST_RENDERING',
'UNIQUE',
'TWO_WAY_BINDING',
'OTHER_DIRECTIVES',
'OTHER_ATTR',
'SLOT',
'CONTENT',
'GLOBAL',
],
}],
/* Miscellaneous */
'vue/no-root-v-if': ['error'],
'vue/require-expose': ['error'],
'vue/no-useless-v-bind': ['error'],
'vue/require-typed-ref': ['error'],
'vue/valid-define-options': ['error'],
'vue/require-explicit-slots': ['error'],
'vue/prefer-define-options': ['error'],
'vue/require-emit-validator': ['error'],
'vue/no-use-v-else-with-v-for': ['error'],
'vue/v-for-delimiter-style': ['error', 'in'],
'vue/no-empty-component-block': ['error'],
'vue/html-comment-indent': ['error', 'tab'],
'vue/no-multiple-objects-in-class': ['error'],
'vue/prefer-separate-static-class': ['error'],
'vue/no-ref-object-reactivity-loss': ['error'],
'vue/no-duplicate-attr-inheritance': ['error'],
'vue/no-this-in-before-route-enter': ['error'],
'vue/no-setup-props-reactivity-loss': ['error'],
'vue/block-lang': ['error', { script: { lang: 'ts' } }],
'vue/component-api-style': ['error', ['script-setup']],
'vue/padding-line-between-blocks': ['error', 'always'],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': ['error', 'type-literal'],
'vue/html-comment-content-spacing': ['error', 'always'],
'vue/prefer-true-attribute-shorthand': ['error', 'always'],
'vue/no-static-inline-styles': ['error', { allowBinding: true }],
'vue/enforce-style-attribute': ['error', { allow: ['scoped'] }],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/no-unsupported-features': ['error', { version: '^3.4.21' }],
'vue/no-required-prop-with-default': ['error', { autofix: true }],
'vue/v-on-handler-style': ['error', ['method', 'inline-function']],
'vue/no-deprecated-model-definition': ['error', { allowVue3Compat: true }],
'vue/html-button-has-type': ['error', { button: true, submit: true, reset: true }],
'vue/block-order': ['error', { order: ['script[setup]', 'template', 'style[scoped]'] }],
'vue/html-comment-content-newline': ['error', { singleline: 'never', multiline: 'always' }],
'vue/match-component-file-name': ['error', { extensions: ['vue'], shouldMatchCase: true }],
'vue/block-tag-newline': ['error', { maxEmptyLines: 0, multiline: 'always', singleline: 'consistent' }],
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
'vue/define-macros-order': ['error', {
order: [
'defineOptions',
'defineModel',
'defineProps',
'defineSlots',
'defineEmits',
],
}],
'vue/require-macro-variable-name': ['error', {
useSlots: 'slots',
useAttrs: 'attrs',
defineSlots: 'slots',
defineProps: 'props',
defineEmits: 'emits',
}],
};
const stylisticRules = {
...stylisticESLint.configs['recommended-flat'].rules,
'style/semi-style': 'error',
'style/no-extra-semi': 'error',
'style/spaced-comment': 'off',
'style/indent': ['error', 'tab'],
'style/arrow-parens': ['error', 'always'],
'style/indent-binary-ops': ['error', 'tab'],
'style/operator-linebreak': ['error', 'none'],
'style/implicit-arrow-linebreak': ['error', 'beside'],
'style/no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'style/semi': ['error', 'always', { omitLastInOneLineBlock: true }],
'style/lines-around-comment': ['error', {
allowTypeStart: true,
allowEnumStart: true,
allowClassStart: true,
allowBlockStart: true,
allowArrayStart: true,
allowModuleStart: true,
allowObjectStart: true,
allowInterfaceStart: true,
}],
'style/member-delimiter-style': ['error', {
multilineDetection: 'brackets',
multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'semi', requireLast: false },
}],
'style/padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: '*',
next: [
'do',
'try',
'for',
'iife',
'with',
'class',
'block',
'while',
'throw',
'return',
'switch',
'export',
'function',
'directive',
'block-like',
'cjs-export',
'multiline-block-like',
],
},
{
blankLine: 'any',
prev: 'export',
next: 'export',
},
{
blankLine: 'never',
prev: 'function-overload',
next: 'function',
},
],
};
const sharedSortOptions = { 'type': 'line-length', 'ignore-case': true };
const generalRules = {
'import/order': 'off',
'style/no-tabs': 'off',
'antfu/if-newline': 'off',
'unused-imports/no-unused-vars': 'off',
/* Sorting */
'perfectionist/sort-maps': ['error', sharedSortOptions],
'perfectionist/sort-exports': ['error', sharedSortOptions],
'perfectionist/sort-union-types': ['error', sharedSortOptions],
'perfectionist/sort-array-includes': ['error', sharedSortOptions],
'perfectionist/sort-intersection-types': ['error', sharedSortOptions],
'perfectionist/sort-named-imports': ['error', { ...sharedSortOptions, 'group-kind': 'types-first' }],
'perfectionist/sort-named-exports': ['error', { ...sharedSortOptions, 'group-kind': 'types-first' }],
'perfectionist/sort-imports': ['error', {
...sharedSortOptions,
'internal-pattern': ['@/**'],
'custom-groups': { value: { 'vue-components': '@/**/*.vue' } },
'groups': [
['side-effect-style', 'side-effect'],
['index-type', 'builtin-type', 'external-type', 'internal-type', 'parent-type', 'sibling-type'],
'vue-components',
['index', 'builtin', 'external', 'internal', 'parent', 'sibling'],
['object', 'unknown'],
],
}],
};
const htmlConfig = {
files: ['index.html'],
plugins: { '@html-eslint': htmlESLint },
languageOptions: { parser: htmlESLintParser },
rules: {
/* Best Practices */
'@html-eslint/no-duplicate-id': 'error',
'@html-eslint/require-doctype': 'error',
'@html-eslint/no-obsolete-tags': 'error',
'@html-eslint/no-duplicate-attrs': 'error',
'@html-eslint/require-li-container': 'error',
'@html-eslint/require-button-type': 'error',
'@html-eslint/no-script-style-type': 'error',
'@html-eslint/require-meta-charset': 'error',
'@html-eslint/require-closing-tags': ['error', { selfClosing: 'always', allowSelfClosingCustom: true }],
/* SEO */
'@html-eslint/require-lang': 'error',
'@html-eslint/require-title': 'error',
'@html-eslint/no-multiple-h1': 'error',
'@html-eslint/require-meta-description': 'error',
'@html-eslint/require-open-graph-protocol': 'error',
/* Accessibility */
'@html-eslint/require-img-alt': 'error',
'@html-eslint/no-abstract-roles': 'error',
'@html-eslint/no-accesskey-attrs': 'error',
'@html-eslint/require-frame-title': 'error',
'@html-eslint/no-aria-hidden-body': 'error',
'@html-eslint/no-positive-tabindex': 'error',
'@html-eslint/no-skip-heading-levels': 'error',
'@html-eslint/require-meta-viewport': 'error',
/* Style */
'@html-eslint/quotes': 'error',
'@html-eslint/lowercase': 'error',
'@html-eslint/indent': ['error', 'tab'],
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/id-naming-convention': 'error',
'@html-eslint/no-multiple-empty-lines': 'error',
'@html-eslint/element-newline': ['error', { skip: ['pre', 'code'] }],
'@html-eslint/no-extra-spacing-attrs': ['error', { enforceBeforeSelfClose: true }],
},
};
export default antfuConfig(
{
files: ['**/*.js', '**/*.ts', 'src/**/*.vue'],
vue: true,
typescript: { tsconfigPath: 'tsconfig.json' },
rules: { ...jsRules, ...tsRules, ...stylisticRules },
},
{ files: ['src/**/*.vue'], rules: { ...vueRules, 'no-useless-assignment': 'off' } },
htmlConfig,
{ rules: generalRules },
{ ignores: ['**/*.json', 'env.d.ts'] },
);
{
"type": "module",
"scripts": {
"dev": "vite",
"preview": "vite preview",
"type-check": "vue-tsc --build --force",
"format": "npx prettier '**/*.{html,css,vue,json}' --config prettier.config.js --write --cache",
"lint": "eslint --config eslint.config.js --ignore-pattern '.vscode/*.json' --fix",
"build": "vite build",
"all": "pnpm run type-check && pnpm run format && pnpm run lint && pnpm run build"
},
"dependencies": {
"vue": "^3.4.21",
},
"devDependencies": {
"@antfu/eslint-config": "^2.16.0",
"@eslint/js": "^9.1.1",
"@html-eslint/eslint-plugin": "^0.24.1",
"@html-eslint/parser": "^0.24.1",
"@stylistic/eslint-plugin": "^1.7.2",
"@tailwindcss/vite": "^4.0.0-alpha.14",
"@types/node": "^20.12.7",
"@vitejs/plugin-vue": "^5.0.4",
"eslint": "^9.2.0",
"eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^4.0.0-alpha.14",
"typescript": "^5.4.3",
"unimport": "^3.7.1",
"vite": "^5.2.7",
"vite-plugin-vue-devtools": "^7.0.25",
"vue-tsc": "^2.0.7"
}
}
export default {
useTabs: true,
printWidth: 120,
singleQuote: true,
trailingComma: 'all',
quoteProps: 'consistent',
plugins: ['prettier-plugin-tailwindcss'],
};
{
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never" // Prevent conflicts with “perfectionist/sort-imports” rule.
},
/* Silent stylistic rules in the editor, but still auto fix them. */
"eslint.rules.customizations": [
{ "rule": "*sort*", "severity": "off" },
{ "rule": "style/*", "severity": "off" },
{ "rule": "*indent", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*console", "severity": "off" },
{ "rule": "import-*", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*newline*", "severity": "off" },
{ "rule": "*attribute*", "severity": "off" },
{ "rule": "format/prettier", "severity": "off" },
{ "rule": "vue/define-macros-order", "severity": "off" },
{ "rule": "*html-closing-bracket-spacing", "severity": "off" }
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"gql",
"graphql"
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment