UNPKG

eslint-config-decent

Version:
948 lines (930 loc) 26.8 kB
import eslint from '@eslint/js'; import { defineConfig } from 'eslint/config'; import prettier from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; import tsEslint from 'typescript-eslint'; import { existsSync, lstatSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fixupPluginRules } from '@eslint/compat'; import importPlugin from 'eslint-plugin-import-x'; import jsdoc from 'eslint-plugin-jsdoc'; import nextJs from '@next/eslint-plugin-next'; import promise from 'eslint-plugin-promise'; import a11y from 'eslint-plugin-jsx-a11y'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import security from 'eslint-plugin-security'; import stylistic from '@stylistic/eslint-plugin'; import testingLibrary from 'eslint-plugin-testing-library'; import unicorn from 'eslint-plugin-unicorn'; import vitest from '@vitest/eslint-plugin'; const base$c = { rules: { "array-callback-return": ["error", { allowImplicit: true }], "block-scoped-var": "error", "callback-return": "off", "class-methods-use-this": "off", "default-case": ["error", { commentPattern: "^no default$" }], "default-case-last": "error", eqeqeq: ["error", "smart"], "func-names": "error", "func-style": [ "error", "declaration", { allowArrowFunctions: false } ], "global-require": "error", "grouped-accessor-pairs": "error", "guard-for-in": "error", "id-length": [ "error", { exceptions: ["_", "$", "e", "i", "j", "k", "q", "t", "x", "y"] } ], "lines-around-directive": [ "error", { before: "always", after: "always" } ], "handle-callback-err": ["error", "^.*err"], "max-classes-per-file": ["error", 1], "object-shorthand": [ "error", "always", { ignoreConstructors: false, avoidQuotes: true, avoidExplicitReturnArrows: true } ], "one-var": ["error", "never"], "operator-assignment": ["error", "always"], "no-await-in-loop": "error", "no-bitwise": "error", "no-buffer-constructor": "error", "no-caller": "error", "no-cond-assign": ["error", "always"], "no-console": "error", "no-constructor-return": "error", "no-else-return": ["error", { allowElseIf: false }], "no-empty-static-block": "error", "no-eval": "error", "no-extend-native": "error", "no-extra-bind": "error", "no-extra-label": "error", "no-inner-declarations": "error", "no-iterator": "error", "no-label-var": "error", "no-labels": ["error", { allowLoop: false, allowSwitch: false }], "no-lone-blocks": "error", "no-lonely-if": "error", "no-multi-assign": ["error"], "no-multi-str": "error", "no-negated-condition": "error", "no-nested-ternary": "error", "no-new-object": "error", "no-new-require": "error", "no-new-wrappers": "error", "no-octal-escape": "error", "no-path-concat": "error", "no-promise-executor-return": "error", "no-proto": "error", "no-restricted-exports": [ "error", { restrictedNamedExports: [ "default", // use `export default` to provide a default export "then" // this will cause tons of confusion when your module is dynamically `import()`ed, and will break in most node ESM versions ] } ], "no-restricted-globals": [ "error", { name: "isFinite", message: "Use Number.isFinite instead https://github.com/airbnb/javascript#standard-library--isfinite" }, { name: "isNaN", message: "Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan" } ], "no-restricted-properties": [ "error", { object: "arguments", property: "callee", message: "arguments.callee is deprecated" }, { property: "__defineGetter__", message: "Please use Object.defineProperty instead." }, { property: "__defineSetter__", message: "Please use Object.defineProperty instead." } ], "no-restricted-syntax": ["error", "DebuggerStatement", "LabeledStatement", "WithStatement"], "no-return-assign": ["error", "always"], "no-self-compare": "error", "no-sequences": "error", "no-script-url": "error", "no-template-curly-in-string": "error", "no-undef-init": "error", "no-unneeded-ternary": ["error", { defaultAssignment: false }], "no-unreachable-loop": [ "error", { ignore: [] // WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement } ], "no-unused-expressions": [ "error", { allowShortCircuit: false, allowTernary: false, allowTaggedTemplates: false } ], "no-useless-computed-key": "error", "no-useless-concat": "error", "no-useless-rename": "error", "no-useless-return": "error", "prefer-const": [ "error", { destructuring: "any", ignoreReadBeforeAssign: true } ], "prefer-exponentiation-operator": "off", "prefer-numeric-literals": "error", "prefer-object-spread": "error", "prefer-regex-literals": [ "error", { disallowRedundantWrapping: true } ], "prefer-template": "error", "sort-imports": [ "error", { ignoreCase: true, ignoreDeclarationSort: true, allowSeparatedGroups: true } ], "symbol-description": "error", "unicode-bom": ["error", "never"], "vars-on-top": "error", yoda: "error" } }; const cjsAndEsm = { rules: { curly: ["error", "multi-line"], "dot-notation": ["error", { allowKeywords: true }], "getter-return": ["error", { allowImplicit: true }], "no-array-constructor": "error", "no-empty-function": [ "error", { allow: ["arrowFunctions", "functions", "methods"] } ], "no-new-func": "error", "no-new-symbol": "error", "no-return-await": "error", "no-shadow": "error", "no-undef": "error", "no-unexpected-multiline": "error", "no-use-before-define": ["error", { functions: true, classes: true, variables: true }], "no-useless-constructor": "error", "no-var": "error", "prefer-arrow-callback": [ "error", { allowNamedFunctions: false, allowUnboundThis: true } ], "prefer-promise-reject-errors": ["error", { allowEmptyReject: true }] } }; const cjs = { rules: { strict: ["error", "global"] } }; const configs$c = { base: base$c, cjsAndEsm, cjs }; const requireExtensionRule = { meta: { type: "suggestion", docs: { description: "Ensure import and export statements include a file extension", url: "https://github.com/jgeurts/eslint-config-decent/tree/main/src/rules/requireExtensionRule.ts" }, fixable: "code", schema: [], messages: { requireExtension: "Relative imports and exports must include a file extension." } }, create(context) { function checkSource(source) { if (source?.type !== "Literal" || typeof source.value !== "string") { return; } const importPath = source.value; if (!importPath || !importPath.startsWith(".") || importPath.endsWith(".js")) { return; } const resolvedPath = resolve(dirname(context.filename), importPath); if (!existsSync(resolvedPath)) { context.report({ node: source, messageId: "requireExtension", fix(fixer) { const fixedPath = `${importPath}.js`; return fixer.replaceText(source, `'${fixedPath}'`); } }); } } return { ImportDeclaration(node) { if (node.type === "ImportDeclaration") { checkSource(node.source); } }, ExportNamedDeclaration(node) { if (node.type === "ExportNamedDeclaration" && node.source) { checkSource(node.source); } }, ExportAllDeclaration(node) { if (node.type === "ExportAllDeclaration") { checkSource(node.source); } } }; } }; const requireIndexRule = { meta: { type: "suggestion", docs: { description: "Ensure directory import and export statements use index.js", url: "https://github.com/jgeurts/eslint-config-decent/tree/main/src/rules/requireIndexRule.ts" }, fixable: "code", schema: [], messages: { requireIndex: "Directory imports and exports must use index.js." } }, create(context) { function checkSource(source) { if (source?.type !== "Literal" || typeof source.value !== "string") { return; } const importPath = source.value; const resolvedPath = resolve(dirname(context.filename), importPath); const isDirectory = existsSync(resolvedPath) && lstatSync(resolvedPath).isDirectory(); if (isDirectory) { context.report({ node: source, messageId: "requireIndex", fix(fixer) { const fixedPath = importPath.replace(/\/?$/, "/index.js"); return fixer.replaceText(source, `'${fixedPath}'`); } }); } } return { ImportDeclaration(node) { if (node.type === "ImportDeclaration") { checkSource(node.source); } }, ExportNamedDeclaration(node) { if (node.type === "ExportNamedDeclaration" && node.source) { checkSource(node.source); } }, ExportAllDeclaration(node) { if (node.type === "ExportAllDeclaration") { checkSource(node.source); } } }; } }; const decentExtensionPlugin = { meta: { name: "decent-extension", version: "1.0.0" }, rules: { "require-extension": requireExtensionRule, "require-index": requireIndexRule } }; const base$b = { plugins: { "decent-extension": decentExtensionPlugin }, rules: { "decent-extension/require-extension": "error", "decent-extension/require-index": "error" } }; const configs$b = { base: base$b }; const base$a = { plugins: { import: fixupPluginRules(importPlugin) }, rules: { "import/consistent-type-specifier-style": ["error", "prefer-inline"], "import/first": "error", "import/newline-after-import": "error", "import/no-duplicates": "error", "import/order": [ "error", { "newlines-between": "always", alphabetize: { order: "asc", caseInsensitive: true }, pathGroupsExcludedImportTypes: ["builtin"], groups: ["builtin", "external", "internal", "parent", "sibling", "index"] } ] } }; const configs$a = { base: base$a }; const base$9 = { settings: { jsdoc: { preferredTypes: { Array: "Array<object>", "Array.": "Array<object>", "Array<>": "[]", "Array.<>": "[]", "Promise.<>": "Promise<>" } } }, plugins: { jsdoc }, rules: { ...jsdoc.configs["flat/recommended"].rules, "jsdoc/check-alignment": "error", "jsdoc/check-indentation": "error", "jsdoc/check-param-names": "off", "jsdoc/check-tag-names": "error", "jsdoc/check-types": "error", "jsdoc/require-description": "off", "jsdoc/require-description-complete-sentence": "off", "jsdoc/require-example": "off", "jsdoc/require-hyphen-before-param-description": "error", "jsdoc/require-param": [ "error", { enableFixer: false, ignoreWhenAllParamsMissing: true, unnamedRootBase: ["args"] } ], "jsdoc/require-param-description": "off", "jsdoc/require-param-name": "error", "jsdoc/require-param-type": "error", "jsdoc/require-returns-description": "off", "jsdoc/require-jsdoc": "off", "jsdoc/require-returns-type": "off", "jsdoc/valid-types": "error" } }; const configs$9 = { base: base$9 }; const recommendedRules = {}; for (const ruleName of Object.keys({ ...nextJs.configs.recommended.rules })) { recommendedRules[ruleName] = "error"; } const base$8 = { plugins: { "@next/next": fixupPluginRules(nextJs) }, rules: { ...recommendedRules } }; const configs$8 = { base: base$8 }; const base$7 = { plugins: { promise }, rules: { "promise/always-return": "error", "promise/always-catch": "off", "promise/catch-or-return": [ "error", { allowThen: true } ], "promise/no-native": "off", "promise/param-names": "error" } }; const configs$7 = { base: base$7 }; const base$6 = { settings: { react: { version: "detect", runtime: "automatic" } }, languageOptions: { ...react.configs["jsx-runtime"].languageOptions }, plugins: { "jsx-a11y": a11y, react: fixupPluginRules(react), "react-hooks": reactHooks }, rules: { ...a11y.configs.recommended.rules, "jsx-a11y/aria-proptypes": "error", "jsx-a11y/aria-role": ["error", { ignoreNonDOM: true }], "jsx-a11y/aria-unsupported-elements": "error", "jsx-a11y/role-has-required-aria-props": "error", ...react.configs.recommended.rules, ...react.configs["jsx-runtime"].rules, "react/default-props-match-prop-types": "error", "react/display-name": ["error", { ignoreTranspilerName: false }], "react/forbid-foreign-prop-types": ["error", { allowInPropTypes: true }], "react/iframe-missing-sandbox": "warn", "react/jsx-closing-bracket-location": "error", "react/jsx-fragments": "error", "react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary"] }], "react/jsx-no-script-url": "error", "react/jsx-no-target-blank": "error", "react/jsx-no-undef": "error", "react/jsx-no-useless-fragment": "error", "react/jsx-pascal-case": [ "error", { allowAllCaps: true, ignore: [] } ], "react/jsx-props-no-multi-spaces": "error", "react/no-access-state-in-setstate": "error", "react/no-arrow-function-lifecycle": "error", "react/no-danger-with-children": "error", "react/no-deprecated": "error", "react/no-did-mount-set-state": "error", "react/no-did-update-set-state": "error", "react/no-direct-mutation-state": "error", "react/no-invalid-html-attribute": "error", "react/no-namespace": "error", "react/no-redundant-should-component-update": "error", "react/no-this-in-sfc": "error", "react/no-typos": "error", "react/no-unsafe": "error", "react/no-unstable-nested-components": "error", "react/no-unused-class-component-methods": "error", "react/no-unused-prop-types": "error", "react/no-unused-state": "error", "react/no-will-update-set-state": "error", "react/prefer-stateless-function": "error", "react/require-render-return": "error", "react/self-closing-comp": "error", "react/style-prop-object": "error", ...reactHooks.configs.flat["recommended-latest"].rules, "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error" } }; const configs$6 = { base: base$6 }; const base$5 = { plugins: { security }, rules: { "security/detect-buffer-noassert": "error", "security/detect-child-process": "error", "security/detect-disable-mustache-escape": "error", "security/detect-eval-with-expression": "error", "security/detect-new-buffer": "error", "security/detect-no-csrf-before-method-override": "error", "security/detect-non-literal-fs-filename": "error", "security/detect-non-literal-regexp": "error", "security/detect-non-literal-require": "error", "security/detect-object-injection": "off", "security/detect-possible-timing-attacks": "error", "security/detect-pseudoRandomBytes": "error", "security/detect-unsafe-regex": "error" } }; const configs$5 = { base: base$5 }; const baseStylistic = stylistic.configs.customize({ braceStyle: "1tbs", jsx: true }); const base$4 = { ...baseStylistic, rules: { ...stylistic.configs["disable-legacy"].rules, ...baseStylistic.rules, "@stylistic/padding-line-between-statements": [ "error", { blankLine: "always", prev: ["directive", "block", "block-like", "multiline-block-like", "cjs-export", "cjs-import", "class", "export", "import", "if"], next: "*" }, { blankLine: "never", prev: "directive", next: "directive" }, { blankLine: "any", prev: "*", next: ["if", "for", "cjs-import", "import"] }, { blankLine: "any", prev: ["export", "import"], next: ["export", "import"] }, { blankLine: "always", prev: "*", next: ["try", "function", "switch"] }, { blankLine: "always", prev: "if", next: "if" }, { blankLine: "never", prev: ["return", "throw"], next: "*" } ] } }; const configs$4 = { base: base$4 }; const base$3 = { plugins: { "testing-library": fixupPluginRules(testingLibrary) }, rules: { ...testingLibrary.configs["flat/react"].rules, "testing-library/no-debugging-utils": "error", "testing-library/prefer-explicit-assert": "warn", "testing-library/prefer-implicit-assert": "error", "testing-library/prefer-query-matchers": "off" } }; const configs$3 = { base: base$3 }; const base$2 = { rules: { "no-loss-of-precision": "off", "no-loop-func": "off", "no-return-await": "off", "no-unused-expressions": "off", "no-use-before-defined": "off", "@typescript-eslint/array-type": [ "error", { default: "array" } ], "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports", fixStyle: "inline-type-imports" }], "@typescript-eslint/ban-ts-comment": [ "error", { minimumDescriptionLength: 10, "ts-expect-error": { descriptionFormat: "^ - [^ ].*$" } } ], "@typescript-eslint/default-param-last": "error", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "error", "@typescript-eslint/naming-convention": [ "error", { selector: "enumMember", format: ["camelCase", "PascalCase", "UPPER_CASE"], trailingUnderscore: "forbid" } ], "@typescript-eslint/no-dupe-class-members": "error", "@typescript-eslint/no-loop-func": "error", "@typescript-eslint/no-redeclare": "error", "@typescript-eslint/member-ordering": [ "error", { default: [ "signature", "private-field", "public-field", "protected-field", "public-constructor", "protected-constructor", "private-constructor", "public-method", "protected-method", "private-method" ] } ], "@typescript-eslint/only-throw-error": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-extra-semi": "error", "@typescript-eslint/no-shadow": "error", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", "@typescript-eslint/parameter-properties": [ "error", { allow: ["readonly"] } ], "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }], "@typescript-eslint/return-await": "error", "@typescript-eslint/sort-type-constituents": "error", "@typescript-eslint/use-unknown-in-catch-callback-variable": "off" } }; const configs$2 = { base: base$2 }; const base$1 = { plugins: { unicorn }, rules: { "unicorn/better-regex": "error", "unicorn/custom-error-definition": "error", "unicorn/no-array-method-this-argument": "error", "unicorn/no-for-loop": "error", "unicorn/prefer-array-find": "error", "unicorn/prefer-node-protocol": "error", "unicorn/prefer-object-from-entries": "error", "unicorn/prefer-set-has": "error" } }; const configs$1 = { base: base$1 }; const base = { plugins: { vitest }, rules: { ...vitest.configs.recommended.rules, "vitest/consistent-test-it": [ "error", { fn: "it", withinDescribe: "it" } ], "vitest/no-conditional-tests": "error", "vitest/no-disabled-tests": "error", "vitest/no-duplicate-hooks": "error", "vitest/no-focused-tests": "error", "vitest/no-standalone-expect": "error", "vitest/no-test-prefixes": "error", "vitest/padding-around-after-all-blocks": "error", "vitest/padding-around-after-each-blocks": "error", "vitest/padding-around-describe-blocks": "error", "vitest/prefer-comparison-matcher": "error", "vitest/prefer-equality-matcher": "error", "vitest/prefer-hooks-in-order": "error", "vitest/prefer-hooks-on-top": "error", "vitest/prefer-lowercase-title": ["error", { ignore: ["describe"] }], "vitest/prefer-mock-promise-shorthand": "error", "vitest/prefer-snapshot-hint": "error", "vitest/prefer-spy-on": "error", "vitest/prefer-strict-equal": "error", "vitest/prefer-vi-mocked": "error", "vitest/require-to-throw-message": "error", "vitest/require-top-level-describe": "error" } }; const configs = { base }; function tsEslintConfig(options) { return decentConfig(options); } function decentConfig(options) { const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true; const enableVitest = options?.enableVitest ?? true; const enableReact = options?.enableReact ?? true; const enableNextJs = options?.enableNextJs ?? false; const enableTestingLibrary = options?.enableTestingLibrary ?? true; const languageOptions = { globals: { ...globals.node }, parserOptions: { projectService: { defaultProject: "tsconfig.json" }, tsconfigRootDir: options?.tsconfigRootDir ?? import.meta.dirname, ...options?.parserOptions } }; return [ { ignores: ["**/dist/**", "**/node_modules/**"] }, eslint.configs.recommended, { languageOptions: { ...tsEslint.configs.base.languageOptions } }, { files: ["**/*.ts", "**/*.tsx"], extends: [...tsEslint.configs.strictTypeChecked, ...tsEslint.configs.stylisticTypeChecked] }, { languageOptions, settings: { ...configs$9.base.settings } }, { name: "eslint-config-decent/base", files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"], plugins: { ...configs$b.base.plugins, ...configs$a.base.plugins, ...configs$9.base.plugins, ...configs$7.base.plugins, ...configs$5.base.plugins, ...configs$1.base.plugins }, rules: { ...configs$c.base.rules, ...enableRequireExtensionRule ? configs$b.base.rules : {}, ...configs$a.base.rules, ...configs$9.base.rules, ...configs$7.base.rules, ...configs$5.base.rules, ...configs$1.base.rules } }, { name: "eslint-config-decent/ts", files: ["**/*.ts", "**/*.tsx"], ...configs$2.base }, ...enableReact ? [ { name: "eslint-config-decent/tsx", files: ["**/*.tsx"], ...configs$6.base }, { name: "eslint-config-decent/tsx-components", files: ["**/components/**/*.tsx"], rules: { "import/no-default-export": "error", "unicorn/filename-case": [ "error", { case: "pascalCase" } ] } }, { name: "eslint-config-decent/tsx-disable-ts-rules", files: ["**/*.tsx"], rules: { "@typescript-eslint/explicit-function-return-type": "off" } } ] : [], ...enableNextJs ? [ { name: "eslint-config-decent/nextjs", files: ["**/*.tsx"], settings: { next: { rootDir: options?.nextJsRootDir } }, ...configs$8.base } ] : [], { name: "eslint-config-decent/cjs-and-esm", files: ["**/*.js", "**/*.cjs", "**/*.mjs"], languageOptions: { sourceType: "script" }, ...configs$c.cjsAndEsm }, { name: "eslint-config-decent/cjs", files: ["**/*.js", "**/*.cjs"], languageOptions: { sourceType: "script" }, ...configs$c.cjs }, ...enableVitest ? [ { name: "eslint-config-decent/vitest-tests", files: ["**/__tests__/**/*.ts?(x)", "**/*.{spec,test}.ts?(x)"], ...configs.base } ] : [], ...enableTestingLibrary ? [ { name: "eslint-config-decent/testing-library", files: ["**/__tests__/**/*.ts?(x)", "**/*.{spec,test,tests}.ts?(x)"], ...configs$3.base } ] : [], ...enableVitest || enableTestingLibrary ? [ { name: "eslint-config-decent/tests-disable-ts-rules", files: ["**/__tests__/**/*.ts?(x)", "**/*.{spec,test,tests}.ts?(x)"], rules: { "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-confusing-void-expression": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unsafe-member-access": "off" } } ] : [], { ...configs$4.base, name: "eslint-config-decent/stylistic", files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"] }, { name: "eslint-config-decent/cjs-and-esm-disable-ts-rules", files: ["**/*.js", "**/*.cjs", "**/*.mjs"], extends: [tsEslint.configs.disableTypeChecked] }, { ...prettier, rules: { ...prettier.rules, curly: ["error", "all"], "prettier/prettier": "error", "arrow-body-style": "off", "prefer-arrow-callback": "off" } } ]; } function config(options) { return defineConfig(...decentConfig(options)); } export { config, configs$c as eslintConfigs, configs$b as extensionConfigs, configs$a as importConfigs, configs$9 as jsdocConfigs, configs$8 as nextJsConfigs, configs$7 as promiseConfigs, configs$6 as reactConfigs, configs$5 as securityConfigs, configs$3 as testingLibraryConfigs, tsEslintConfig, configs$2 as typescriptEslintConfigs, configs$1 as unicornConfigs, configs as vitestConfigs };