UNPKG

@jimmy.codes/eslint-config

Version:

A simple, modern ESLint config that covers most use cases.

609 lines (577 loc) 17.2 kB
import { hasAstro, hasJest, hasNext, hasPlaywright, hasReact, hasReactQuery, hasStorybook, hasTestingLibrary, hasTypescript, hasVitest } from "./chunk-OCS4JNPP.js"; import { GLOB_CJS, GLOB_IGNORES, GLOB_TESTS } from "./chunk-N5KZEOXT.js"; // src/configs/commonjs.ts import globals from "globals"; var commonjsConfig = () => { return [ { files: [GLOB_CJS], languageOptions: { globals: globals.commonjs }, name: "jimmy.codes/commonjs" } ]; }; // src/configs/eslint-comments.ts import comments2 from "@eslint-community/eslint-plugin-eslint-comments/configs"; // src/rules/eslint-comments.ts import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; var eslintCommentsRules = { ...comments.recommended.rules, "@eslint-community/eslint-comments/no-unused-disable": "off", "@eslint-community/eslint-comments/require-description": "error" }; // src/configs/eslint-comments.ts var eslintCommentsConfig = () => { return [ { ...comments2.recommended, name: "jimmy.codes/eslint-comments", rules: eslintCommentsRules } ]; }; // src/configs/ignores.ts var ignoresConfig = (ignores) => { return [ { ignores: [...GLOB_IGNORES, ...ignores], name: "jimmy.codes/ignores" } ]; }; // src/configs/imports.ts import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; import { configs as configs2, importX } from "eslint-plugin-import-x"; import nodePlugin from "eslint-plugin-n"; // src/rules/imports.ts import { configs } from "eslint-plugin-import-x"; var importsRules = { ...configs.recommended.rules, "import-x/consistent-type-specifier-style": ["error", "prefer-top-level"], "import-x/extensions": [ "error", "never", { checkTypedImports: true, svg: "always" } ], "import-x/first": "error", // ! can't get this rule to work "import-x/namespace": "off", "import-x/newline-after-import": "error", "import-x/no-absolute-path": "error", "import-x/no-duplicates": "error", "import-x/no-empty-named-blocks": "error", "import-x/no-named-as-default": "error", "import-x/no-named-as-default-member": "error", "import-x/no-self-import": "error", "import-x/no-unresolved": ["error", { ignore: [String.raw`\.svg$`] }], "import-x/no-useless-path-segments": "error" }; // src/configs/imports.ts var importsTypescriptConfig = () => { const { rules, settings } = configs2.typescript; return [ { name: "jimmy.codes/imports/typescript", rules, settings: { "import-x/extensions": settings["import-x/extensions"], "import-x/external-module-folders": settings["import-x/external-module-folders"], "import-x/parsers": settings["import-x/parsers"], "import-x/resolver-next": [createTypeScriptImportResolver()] } } ]; }; var importsConfig = ({ isTypescriptEnabled = false } = {}) => { return [ { name: "jimmy.codes/imports", plugins: { "import-x": importX, "n": nodePlugin }, rules: importsRules }, ...isTypescriptEnabled ? importsTypescriptConfig() : [] ]; }; // src/rules/javascript.ts import eslint from "@eslint/js"; var additionalRules = { "array-callback-return": [ "error", { allowImplicit: true // Allow implicit return for flexibility while enforcing return consistency } ], "arrow-body-style": ["error", "always"], // Enforce `{}` in arrow functions for consistency (open to revisiting) "class-methods-use-this": "error", // Ensures class methods are used within their class (avoids unnecessary methods) "consistent-return": "error", // Prevents inconsistent function returns (e.g., sometimes returning `undefined`) "curly": ["error", "all"], // Enforce `{}` around all control statements (avoids accidental bugs) "default-case": "error", // Require `default` case in switch statements (prevents unintended fallthrough) "default-case-last": "error", // Ensure the `default` case is the last one in `switch` "no-console": "warn", // Allow logging in development, but should be reviewed for production builds "no-implicit-coercion": "error", // Prevents shorthand type conversions (e.g., `!!value`, `+var`) "no-implicit-globals": "error", // Prevents defining variables in the global scope "no-loop-func": "error", // Disallow function declarations inside loops (prevents scoping issues) "no-magic-numbers": ["error", { ignore: [0, 1, -1, 2] }], // Helps catch unexplained numbers while allowing common values "no-new-wrappers": "error", // Prevents `new String()`, `new Number()`, etc. (use literals instead) "no-param-reassign": ["error", { props: true }], // Disallow reassigning function parameters but allow modifying object properties "no-promise-executor-return": "error", // Ensures `new Promise` executors always return something "no-self-compare": "error", // Disallows `x === x` (usually a mistake) "no-template-curly-in-string": "error", // Flags unintended use of `${}` inside strings without backticks "no-throw-literal": "error", // Prevents throwing non-error objects (e.g., `throw "error"` should be `throw new Error("error")`) "no-unassigned-vars": "error", // Disallow let or var variables that are read but never assigned "no-unmodified-loop-condition": "error", // Ensures loop conditions change, preventing infinite loops "no-unreachable-loop": "error", // Prevents loops that can never execute "no-use-before-define": [ "error", { allowNamedExports: false, classes: false, functions: true, // Allow function hoisting variables: true } ], "no-useless-computed-key": "error", // Disallows unnecessary computed keys in objects (e.g., `{["key"]: value}` instead of `{ key: value }`) "no-useless-constructor": "error", // Disallows empty constructors "no-useless-rename": "error", // Disallows renaming variables to the same name in import/export/destructuring "no-useless-return": "error", // Prevents redundant `return;` statements "no-var": "error", // Enforce `let` and `const` (modern best practice) "object-shorthand": "error", // Enforces `{ foo }` instead of `{ foo: foo }` "prefer-arrow-callback": "error", // Enforces arrow functions in callbacks where possible "prefer-const": "error", // Requires `const` when a variable isn’t reassigned "prefer-destructuring": [ "error", { AssignmentExpression: { array: false, // Allow `x = arr[0]`, don't force `[x] = arr` object: false // Allow `x = obj.x`, don't force `{ x } = obj` }, VariableDeclarator: { array: false, object: true // Enforce destructuring for objects (`const { x } = obj`) } } ], "prefer-object-spread": "error", // Enforces `{ ...obj }` over `Object.assign()` "prefer-rest-params": "error", // Use `...args` instead of `arguments` "prefer-spread": "error", // Use `[...arr]` instead of `arr.concat()` "prefer-template": "error", // Use template literals instead of string concatenation "radix": "error", // Require a radix argument in `parseInt` "require-await": "error", // Disallows async functions without `await` (prevents unnecessary `async`) "strict": ["error", "safe"], // Enforces `"use strict"` only where needed (safe mode) "symbol-description": "error" // Requires descriptions when creating `Symbol()` }; var javascriptRules = { ...eslint.configs.recommended.rules, ...additionalRules }; // src/configs/javascript.ts var javascriptConfig = () => { return [ { linterOptions: { reportUnusedDisableDirectives: true }, name: "jimmy.codes/javascript", rules: javascriptRules }, { files: GLOB_TESTS, name: "jimmy.codes/javascript/testing", rules: { "no-magic-numbers": "off" } } ]; }; // src/configs/jsdoc.ts import jsdocPlugin2 from "eslint-plugin-jsdoc"; // src/rules/jsdoc.ts import jsdocPlugin from "eslint-plugin-jsdoc"; var jsdocRules = () => { return { ...jsdocPlugin.configs["flat/recommended-typescript-error"].rules, "jsdoc/require-jsdoc": "off", // Disable enforced JSDoc (TypeScript provides type info) "jsdoc/require-param": "off", // Avoid redundant param documentation (rely on TypeScript) "jsdoc/require-returns": "off", // Avoid redundant return type documentation (rely on TypeScript) "jsdoc/tag-lines": [ "error", "always", { applyToEndTag: false, // No need for a blank line before closing JSDoc startLines: 1 // Require exactly one blank line before the first tag } ] }; }; // src/configs/jsdoc.ts var jsdocConfig = () => { return [ { ...jsdocPlugin2.configs["flat/recommended-typescript-error"], name: "jimmy.codes/jsdoc", rules: jsdocRules() } ]; }; // src/configs/node.ts import nodePlugin2 from "eslint-plugin-n"; // src/rules/node.ts var nodeRules = { "n/handle-callback-err": ["error", "^(err|error)$"], "n/no-deprecated-api": "error", "n/no-exports-assign": "error", "n/no-new-require": "error", "n/no-path-concat": "error", "n/no-process-exit": "off", // TODO [2025-12-31]: enable this rule "n/no-top-level-await": ["error", { ignoreBin: true }], "n/prefer-global/console": ["error", "always"], "n/prefer-node-protocol": "error", "n/process-exit-as-throw": "error" }; // src/configs/node.ts var nodeConfig = () => { return [ { name: "jimmy.codes/node", plugins: { n: nodePlugin2 }, rules: nodeRules } ]; }; // src/configs/perfectionist.ts import perfectionist2 from "eslint-plugin-perfectionist"; // src/rules/perfectionist.ts import perfectionist from "eslint-plugin-perfectionist"; var perfectionistRules = { ...perfectionist.configs["recommended-natural"].rules, "perfectionist/sort-imports": [ "error", { customGroups: { type: {}, value: {} }, environment: "node", groups: [ "side-effect-style", "builtin", "type", "external", "internal-type", "internal", ["parent-type", "sibling-type", "index-type"], ["parent", "sibling", "index"], "object", "style", "unknown" ], internalPattern: ["^~/.*", "^@/.*"], order: "asc", type: "natural" } ], // TODO: enable perfectionist/sort-modules // "perfectionist/sort-modules": [ // "error", // { // customGroups: [], // groups: [ // "declare-enum", // "enum", // "export-enum", // ["declare-interface", "declare-type"], // ["interface", "type"], // ["export-interface", "export-type"], // "declare-class", // "class", // "export-class", // "declare-function", // "function", // "export-function", // ], // ignoreCase: true, // newlinesBetween: "ignore", // order: "asc", // partitionByComment: false, // partitionByNewLine: false, // specialCharacters: "keep", // type: "natural", // }, // ], "perfectionist/sort-modules": "off" }; // src/configs/perfectionist.ts var perfectionistConfig = () => { return [ { name: "jimmy.codes/perfectionist", plugins: { // TODO: remove unknown conversion perfectionist: perfectionist2 }, rules: perfectionistRules } ]; }; // src/configs/prettier.ts import eslintConfigPrettier from "eslint-config-prettier/flat"; var prettierConfig = () => { return [ { ...eslintConfigPrettier, name: "jimmy.codes/prettier" } ]; }; // src/configs/regexp.ts import * as regexpPlugin2 from "eslint-plugin-regexp"; // src/rules/regexp.ts import * as regexpPlugin from "eslint-plugin-regexp"; var regexpRules = { ...regexpPlugin.configs["flat/recommended"].rules, "regexp/confusing-quantifier": "error", "regexp/no-empty-alternative": "error", "regexp/no-lazy-ends": "error", "regexp/no-potentially-useless-backreference": "error", "regexp/no-useless-flag": "error", "regexp/optimal-lookaround-quantifier": "error" }; // src/configs/regexp.ts var regexpConfig = () => { return [ { name: "jimmy.codes/regexp", plugins: { regexp: regexpPlugin2 }, rules: regexpRules } ]; }; // src/configs/stylistic.ts import stylisticPlugin from "@stylistic/eslint-plugin"; // src/rules/stylistic.ts var stylisticRules = { "@stylistic/jsx-curly-brace-presence": ["error", "never"], "@stylistic/object-curly-newline": [ "error", { consistent: true, multiline: true } ], "@stylistic/object-property-newline": [ "error", { allowAllPropertiesOnSameLine: true } ], "@stylistic/padding-line-between-statements": [ "error", { blankLine: "always", next: "return", prev: "*" }, { blankLine: "always", next: "*", prev: ["const", "let", "var"] }, { blankLine: "any", next: ["const", "let", "var"], prev: ["const", "let", "var"] }, { blankLine: "always", next: "*", prev: "directive" }, { blankLine: "any", next: "directive", prev: "directive" }, { blankLine: "always", next: "function", prev: "*" } ] }; // src/configs/stylistic.ts function stylisticConfig() { return [ { name: "jimmy.codes/stylistic", plugins: { "@stylistic": stylisticPlugin }, rules: stylisticRules } ]; } // src/configs/unicorn.ts import eslintPluginUnicorn2 from "eslint-plugin-unicorn"; // src/rules/unicorn.ts import eslintPluginUnicorn from "eslint-plugin-unicorn"; var unicornRules = { ...eslintPluginUnicorn.configs.recommended.rules, "unicorn/filename-case": "off", "unicorn/import-style": "off", "unicorn/no-abusive-eslint-disable": "off", "unicorn/no-anonymous-default-export": "off", "unicorn/no-array-callback-reference": "off", // TODO: enable when https://github.com/sindresorhus/eslint-plugin-unicorn/issues/781 is resolved. "unicorn/no-array-reduce": "off", "unicorn/no-null": "off", "unicorn/no-process-exit": "off", "unicorn/no-useless-undefined": "off", "unicorn/prefer-node-protocol": "off", "unicorn/prevent-abbreviations": "off" }; // src/configs/unicorn.ts var unicornConfig = () => { return [ { ...eslintPluginUnicorn2.configs.recommended, name: "jimmy.codes/unicorn", rules: unicornRules } ]; }; // src/utils/unwrap.ts var unwrap = async (module) => { const resolved = await module; if (typeof resolved.default === "function") { return resolved.default(); } return resolved; }; // src/factory.ts var defineConfig = async ({ astro = false, autoDetect = true, ignores = [], jest = false, nextjs = false, overrides = [], playwright = false, react = false, storybook = false, tanstackQuery = false, testingLibrary = false, typescript = false, vitest = false } = {}, ...moreOverrides) => { const getFlag = (explicit, detector) => { return explicit || autoDetect && detector(); }; const isTypescriptEnabled = getFlag(typescript, hasTypescript); const isReactEnabled = getFlag(react, hasReact); const isAstroEnabled = getFlag(astro, hasAstro); const isTanstackQueryEnabled = getFlag(tanstackQuery, hasReactQuery); const isTestingLibraryEnabled = getFlag(testingLibrary, hasTestingLibrary); const isPlaywrightEnabled = getFlag(playwright, hasPlaywright); const isStorybookEnabled = getFlag(storybook, hasStorybook); const isNextjsEnabled = getFlag(nextjs, hasNext); const isJestEnabled = getFlag(jest, hasJest); const isVitestEnabled = getFlag(vitest, hasVitest); const baseConfigs = [ javascriptConfig(), perfectionistConfig(), nodeConfig(), unicornConfig(), eslintCommentsConfig(), regexpConfig(), jsdocConfig(), importsConfig({ isTypescriptEnabled }), stylisticConfig() ]; const featureConfigs = await Promise.all([ isTypescriptEnabled && unwrap(import("./typescript-M6XXI2FU.js")), isReactEnabled && unwrap(import("./react-6ERTZG2I.js")), isTanstackQueryEnabled && unwrap(import("./tanstack-query-QBZ3MKQ7.js")), isAstroEnabled && unwrap(import("./astro-Z5RFF624.js")), isJestEnabled && unwrap(import("./jest-7NR7TDOP.js")), isVitestEnabled && unwrap(import("./vitest-726PXOPS.js")), isTestingLibraryEnabled && unwrap(import("./testing-library-HML5O6UC.js")), isPlaywrightEnabled && unwrap(import("./playwright-BIJ7HLQZ.js")), isStorybookEnabled && unwrap(import("./storybook-XHFO7L4T.js")), isNextjsEnabled && unwrap(import("./nextjs-7V464KOE.js")) ]); return [ ...baseConfigs, ...featureConfigs.filter(Boolean), commonjsConfig(), ignoresConfig(ignores), prettierConfig(), overrides, moreOverrides ].flat(); }; export { defineConfig };