UNPKG

eslint-config-bevry

Version:

Intelligent, self-configuring ESLint configuration that automatically analyzes your project structure, dependencies, and metadata to apply optimal linting rules for JavaScript, TypeScript, React, Node.js, and more.

454 lines (401 loc) 14.3 kB
// ESLint Core import { globalIgnores } from 'eslint/config' import globals from 'globals' // eslint-disable-line // ESLint Plugins import eslintJS from '@eslint/js' import eslintReact from 'eslint-plugin-react' import eslintReactHooks from 'eslint-plugin-react-hooks' import eslintPrettier from 'eslint-config-prettier/flat' import eslintTypescript from 'typescript-eslint' import eslintNode from 'eslint-plugin-n' import eslintImport from 'eslint-plugin-import' import eslintJSDoc from 'eslint-plugin-jsdoc' import babelParser from '@babel/eslint-parser' import babelPlugin from '@babel/eslint-plugin' // Our dependencies import versionClean from 'version-clean' // Node.js dependencies import { join } from 'node:path' import { cwd } from 'node:process' // Local imports import * as rules from './rules.js' // Local paths import { readJSON } from '@bevry/json' import filedirname from 'filedirname' const [, dirname] = filedirname() const bevryRootPath = join(dirname, '..') const userRootPath = cwd() const bevryPackagePath = join(bevryRootPath, 'package.json') const userPackagePath = join(userRootPath, 'package.json') // ------------------------------------ // Prepare /** * Error thrown when a dependency that should be inlined is found in the user's package.json */ class InlinedError extends Error { /** * Creates an InlinedError instance * @param {string} dependency - The name of the dependency that is now inlined */ constructor(dependency) { super( `the development dependency ${dependency} is now inlined within eslint-config-bevry\nrun: npm uninstall --save-dev ${dependency}`, ) } } /** * Reads a JSON file, returning an empty object if reading failed. * @param {string} path - The path to the JSON file to read * @param {object} [fallback] - The fallback value to return if reading fails * @returns {Promise<object>} The parsed JSON object or the fallback value */ function readJSONFallback(path, fallback = {}) { return readJSON(path).catch(() => fallback) } const bevryPackage = await readJSON(bevryPackagePath) const userPackage = await readJSONFallback(userPackagePath) // https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects /** @type {import('eslint').Linter.Config} */ const config = { name: bevryPackage.name, // version: bevryPackage.version, files: userPackage.eslintConfig?.files || [], // don't use ignores, as it doesn't help us with matched patterns from extended configurations, instead use globalIgnores extends: [ globalIgnores([ // '**/*.d.ts', <-- now that we fixed the rules that conflict with typescript with v6.1.1, ignoring these isn't necessary anymore '**/vendor/', '**/node_modules/', '**/edition-*/', ...(userPackage.eslintConfig?.ignores || []), ]), rules.jsBefore, eslintJS.configs.recommended, rules.jsAfter, ], languageOptions: { // ecmaVersion: null, // sourceType: null, globals: {}, // parser: null, parserOptions: { ecmaFeatures: {}, }, }, linterOptions: { // noInlineConfig: false, // reportUnusedDisableDirectives: 'warn', // reportUnusedInlineConfigs: 'off', }, // processor: {} // plugins: {}, rules: {}, settings: {}, // env has been deprecated: https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options } // ------------------------------------ // Load // dependency helpers const deps = {} const versions = {} /** * Check if a dependency is included in the caller's package.json * @param {string} name package name/identifier for the dependency * @returns {boolean} true if the dependency is present, false otherwise */ function hasDep(name) { return Boolean(deps[name]) } /** * Throws if any of these inlined dependencies are present in the caller's package.json * @param {...string} names - The package names or identifiers to check for inlining. * @throws {InlinedError} if any of the specified dependencies are present */ function inlinedDeps(...names) { if (userPackage.name === 'eslint-config-bevry') return for (const name of names) { if (hasDep(name)) { throw new InlinedError(name) } } } // Load the dependencies and versions Object.assign( deps, userPackage.dependencies || {}, userPackage.devDependencies || {}, ) Object.keys(deps).forEach((name) => { const range = deps[name] const version = versionClean(range) || null // resolve to null in case of github references versions[name] = version }) // extract some common items const keywords = userPackage.keywords || [] // ------------------------------------ // Deprecations inlinedDeps( // javascript '@eslint/js', // inlined // import 'eslint-plugin-import', // inlined // node 'eslint-plugin-n', // inlined // react 'eslint-plugin-react', // inlined 'eslint-plugin-react-hooks', // inlined // babel // https://babeljs.io/docs/babel-eslint-plugin '@babel/eslint-parser', // inlined '@babel/eslint-plugin', // inlined 'babel-eslint', // deprecated: @babel/eslint-parser 'eslint-plugin-babel', // deprecated: @babel/eslint-plugin // typescript 'typescript-eslint', // inlined '@typescript-eslint/eslint-plugin', // deprecated: typescript-eslint // jsdoc 'eslint-plugin-jsdoc', // inlined // prettier, must come last as it modifies things from nearly all other plugins 'eslint-config-prettier', // inlined 'eslint-plugin-prettier', // deprecated: boundation calls prettier separately, as such we just use eslint-config-prettier to disable formatting conflicts ) // the below are no longer supported by us // http://flowtype.org which is the old domain is now 404, the new domains is https://flow.org and it still gets updated, however it hasn't been updated for eslint v9 yet, and typescript won, with native typescript support now in node.js and deno // https://flow.org/en/docs/tools/eslint/ // 'flow-bin', // 'hermes-eslint', // the required parser, not compatible with eslint v9 // 'eslint-plugin-fb-flow', // the required plugin/config, not compatible with eslint v9 // 'eslint-plugin-flow-vars', // the odl plugin, last update 4 years ago: https://github.com/gajus/eslint-plugin-flowtype // baseui, the baseui ecosystem has been abandoned: github issues have been disabled, and a notice of limited engagement is in the readme // 'baseui', // 'eslint-plugin-baseui', // not comaptible with eslint v9 // ------------------------------------ // Enhancements // prepare const ecmascriptNextYear = new Date().getFullYear() - 1 const ecmascriptNextVersion = 'latest' let ecmascriptVersionSource = ecmascriptNextVersion let ecmascriptVersionTarget = ecmascriptVersionSource let sourceType = 'script', react = hasDep('react'), typescript = hasDep('typescript'), babel = hasDep('@babel/core'), jsx = false const prettier = Boolean(userPackage.prettier) || hasDep('prettier'), browser = Boolean(userPackage.browser) || keywords.includes('browser'), node = Boolean(userPackage.engines?.node) || keywords.includes('node'), worker = keywords.includes('worker') || keywords.includes('workers') || keywords.includes('webworker') || keywords.includes('webworkers') // @todo to support `n/no-unsupported-features/es-syntax` (which crazily uses Node.js version instead of ecmaVersion) need to use @bevry/nodejs-versions to compare all Node.js versions against the range... or, go through each edition with node.js engines and split on || and get the highest value /** * Ensure the ecmascript version is coerced to an eslint valid ecmascript version * @param {string} [version] - The version string to coerce (e.g., 'esnext', 'latest', '2020') * @returns {string|number} The coerced version as a string or number, or empty string if invalid */ function coerceEcmascriptVersion(version = '') { if (version === 'esnext' || version === 'latest' || version === 'next') { return ecmascriptNextVersion } else if (version) { version = versionClean(version) if (version) { version = Number(version) if (version >= ecmascriptNextYear) { return ecmascriptNextVersion } else { return version } } } return '' } // editions if (userPackage.editions) { const sourceEdition = userPackage.editions[0] const sourceEditionTags = sourceEdition.tags || sourceEdition.syntaxes || [] const ecmascriptVersionTag = coerceEcmascriptVersion( sourceEditionTags.find((tag) => tag.startsWith('es')), ) const ecmascriptVersionEngine = coerceEcmascriptVersion( userPackage.engines?.ecmascript, ) ecmascriptVersionSource = ecmascriptVersionTag || ecmascriptVersionEngine || ecmascriptNextVersion ecmascriptVersionTarget = ecmascriptVersionEngine || ecmascriptVersionTag || ecmascriptNextVersion if (sourceEditionTags.includes('typescript')) { typescript = true } if ( sourceEditionTags.includes('module') || sourceEditionTags.includes('import') ) { sourceType = 'module' } else if ( sourceEditionTags.includes('script') || sourceEditionTags.includes('require') ) { // commonjs is also supported sourceType = 'script' } if (sourceEditionTags.includes('react')) { react = true } if (sourceEditionTags.includes('babel')) { babel = true } if (sourceEditionTags.includes('jsx') || sourceEditionTags.includes('tsx')) { jsx = true } if (config.files.length === 0) { config.files.push( `${sourceEdition.directory || '.'}/**/*.{js,cjs,mjs,jsx,mjsx,ts,cts,mts,tsx,mtsx}`, ) } } else if (config.files.length === 0) { config.files.push('**/*.{js,cjs,mjs,jsx,mjsx,ts,cts,mts,tsx,mtsx}') } // adjust parser and syntax to the desired ecmascript version config.languageOptions.ecmaVersion = ecmascriptVersionSource config.languageOptions.sourceType = sourceType config.languageOptions.parserOptions.ecmaFeatures.jsx = jsx // adjust globals to the lowest ecmascript version if (ecmascriptVersionTarget === 'latest' || ecmascriptVersionTarget >= 2021) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.es2021, } } else if (ecmascriptVersionTarget >= 2020) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.es2020, } } else if (ecmascriptVersionTarget >= 2017) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.es2017, } } else if (ecmascriptVersionTarget >= 2015) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.es2015, } } else if (ecmascriptVersionTarget <= 5) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.es5, } } if (worker) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.worker, ...globals.serviceworker, } } // import if (sourceType === 'module') { // https://github.com/import-js/eslint-plugin-import?tab=readme-ov-file#config---flat-with-config-in-typescript-eslint config.extends.push(eslintImport.flatConfigs.recommended) if (react) { config.extends.push(eslintImport.flatConfigs.react) } config.extends.push(eslintImport.flatConfigs.typescript, rules.importAfter) } // node if (node) { // https://github.com/eslint-community/eslint-plugin-n#-configs if (sourceType === 'module') { config.extends.push(eslintNode.configs['flat/recommended-module']) } else { config.extends.push(eslintNode.configs['flat/recommended-script']) } // these globals should be handeld by the plugin above, but add them anyway config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.node, ...globals.nodeBuiltin, } config.extends.push(rules.nodeAfter) if (typescript || babel) { config.extends.push(rules.nodeCompiled) } } if (browser) { config.languageOptions.globals = { ...config.languageOptions.globals, ...globals.browser, } } // react if (react) { config.extends.push(eslintReact.configs.flat.recommended) if (jsx) { config.extends.push(eslintReact.configs.flat['jsx-runtime']) } config.extends.push(eslintReactHooks.configs['recommended-latest']) } // babel // https://babeljs.io/docs/babel-eslint-plugin if (babel) { config.languageOptions.parser = babelParser config.plugins.babel = babelPlugin // this already includes the rules and their config, so it seems that the docs are out of date } // typescript if (typescript) { Object.assign(config.languageOptions.parserOptions, { projectService: true, tsconfigRootDir: userRootPath, }) config.extends.push( // https://typescript-eslint.io/users/configs // https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/eslintrc/stylistic-type-checked.ts // we extend eslint recommended, and as such, we need to handle disabling it accordingly eslintTypescript.configs.eslintRecommended, // our project is a typescript project, so it has type checking // eslintTypescript.configs.recommendedTypeChecked, // our project is a typescript project, so it has type checking eslintTypescript.configs.strictTypeChecked, // our project is a typescript project, so style it accordingly, this is different from the stylistic plugin and different from prettier eslintTypescript.configs.stylisticTypeChecked, // our overrides rules.typescriptAfter, ) if (ecmascriptVersionTarget <= 5) { config.extends.push(rules.typescriptEs5) } if (ecmascriptVersionTarget <= 2015) { config.extends.push(rules.typescriptEs2015) } config.extends.push(rules.typescriptTests) } else { // jsdoc without typescript } // jsdoc // https://www.npmjs.com/package/eslint-plugin-jsdoc if (typescript) { config.extends.push(eslintJSDoc.configs['flat/recommended-typescript']) } else { // config.extends.push(eslintJSDoc.configs['flat/recommended-typescript-flavor']) <-- consider using this instead of typescript style jsdoc rules and types in javascript files config.extends.push(eslintJSDoc.configs['flat/recommended']) } // prettier if (prettier) { config.extends.push(eslintPrettier) } // this is after typescript, as we need to override defaults from typescript configs if (ecmascriptVersionSource <= 5) { config.extends.push(rules.es5) } // user rules overrides if (userPackage.eslintConfig?.rules) { config.extends.push({ name: 'eslint-config-bevry/package-json', rules: userPackage.eslintConfig.rules, }) } // ------------------------------------ // Export export default config