diff --git a/README.md b/README.md index d1dcd651..0fb2e470 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ In your `eslint.config.mjs`([or equivalent](https://eslint.org/docs/latest/use/c ```js import eslintConfigRightcapital from '@rightcapital/eslint-config'; -const { config } = eslintConfigRightcapital.utils; +const { defineConfig } = eslintConfigRightcapital.utils; -export default config( +export default defineConfig( ...eslintConfigRightcapital.configs.recommended, // add more configs for specific files or packages if needed @@ -73,7 +73,25 @@ export default config( **`utils`** -- `config`: reexported util from `typescript-eslint` for easier compositing ESLint config. (docs: https://typescript-eslint.io/packages/typescript-eslint#config) +- `defineConfig`: reexported util from `typescript-eslint` for easier compositing ESLint config. (docs: https://typescript-eslint.io/packages/typescript-eslint#config), with automatic plugin inference (when the plugin is known to `@rightcapital/eslint-config`). + + ```js + const { defineConfig } = eslintConfigRightcapital.utils; + + export default defineConfig({ + plugins: { + /** + * You can omit this since it's already known to `@rightcapital/eslint-config`. + * And `defineConfig` will automatically infer the plugin from `@rightcapital/eslint-config`. + */ + // unicorn: eslintPluginUnicorn, + }, + rules: { + 'unicorn/no-hex-escape': 'error', + }, + }); + ``` + - `globals`: reexported util from [globals](https://github.com/sindresorhus/globals), useful for configuring [`languageOptions.globals`](https://eslint.org/docs/latest/use/configure/language-options#specifying-globals). --- diff --git a/change/@rightcapital-eslint-config-6c337893-976b-4e96-b2b9-ec4077713318.json b/change/@rightcapital-eslint-config-6c337893-976b-4e96-b2b9-ec4077713318.json new file mode 100644 index 00000000..7b17d065 --- /dev/null +++ b/change/@rightcapital-eslint-config-6c337893-976b-4e96-b2b9-ec4077713318.json @@ -0,0 +1,7 @@ +{ + "comment": "feat!: expose used plugins and automatic inference used plugins", + "type": "major", + "packageName": "@rightcapital/eslint-config", + "email": "im@pyonpyon.today", + "dependentChangeType": "patch" +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 94ef918d..56c97541 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,9 @@ import eslintConfigRightcapital from '@rightcapital/eslint-config'; import eslintPluginEslintPlugin from 'eslint-plugin-eslint-plugin'; -const { config } = eslintConfigRightcapital.utils; +const { defineConfig } = eslintConfigRightcapital.utils; -export default config( +export default defineConfig( { /** * NOTE: diff --git a/packages/eslint-config/src/config/base/best-practices.ts b/packages/eslint-config/src/config/base/best-practices.ts index 7353df9f..9c0ec080 100644 --- a/packages/eslint-config/src/config/base/best-practices.ts +++ b/packages/eslint-config/src/config/base/best-practices.ts @@ -1,15 +1,12 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import eslintPluginLodash from 'eslint-plugin-lodash'; -import eslintPluginUnicorn from 'eslint-plugin-unicorn'; + +import { pickPlugins } from '../../utils.js'; // extracted from eslint-config-airbnb-base@15.0.0 // https://github.com/airbnb/javascript/blob/eslint-config-airbnb-base-v15.0.0/packages/eslint-config-airbnb-base/rules/best-practices.js const config: TSESLint.FlatConfig.ConfigArray = [ { - plugins: { - lodash: eslintPluginLodash, - unicorn: eslintPluginUnicorn, - }, + plugins: pickPlugins(['lodash', 'unicorn']), rules: { // enforces return statements in callbacks of array's methods // https://eslint.org/docs/rules/array-callback-return diff --git a/packages/eslint-config/src/config/base/imports.ts b/packages/eslint-config/src/config/base/imports.ts index 6ea8ea84..aa8a3481 100644 --- a/packages/eslint-config/src/config/base/imports.ts +++ b/packages/eslint-config/src/config/base/imports.ts @@ -1,16 +1,12 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; -import eslintPluginImportX from '../../plugins/eslint-plugin-import-x.js'; +import { pickPlugins } from '../../utils.js'; // extracted from eslint-config-airbnb-base@15.0.0 // https://github.com/airbnb/javascript/blob/eslint-config-airbnb-base-v15.0.0/packages/eslint-config-airbnb-base/rules/imports.js const config: TSESLint.FlatConfig.ConfigArray = [ { - plugins: { - 'import-x': eslintPluginImportX, - 'simple-import-sort': eslintPluginSimpleImportSort, - }, + plugins: pickPlugins(['import-x', 'simple-import-sort']), rules: { // Static analysis: diff --git a/packages/eslint-config/src/config/base/style.ts b/packages/eslint-config/src/config/base/style.ts index 6fc1e77c..734a8df1 100644 --- a/packages/eslint-config/src/config/base/style.ts +++ b/packages/eslint-config/src/config/base/style.ts @@ -1,13 +1,13 @@ -import eslintPluginStylistic from '@stylistic/eslint-plugin'; import type { TSESLint } from '@typescript-eslint/utils'; +import { pickPlugins } from '../../utils.js'; + // extracted from eslint-config-airbnb-base@15.0.0 // https://github.com/airbnb/javascript/blob/eslint-config-airbnb-base-v15.0.0/packages/eslint-config-airbnb-base/rules/style.js const config: TSESLint.FlatConfig.ConfigArray = [ { - plugins: { - '@stylistic': eslintPluginStylistic as TSESLint.FlatConfig.Plugin, - }, + plugins: pickPlugins(['@stylistic']), + rules: { // require camel case names camelcase: ['error', { properties: 'never', ignoreDestructuring: false }], diff --git a/packages/eslint-config/src/config/mixin/node.ts b/packages/eslint-config/src/config/mixin/node.ts index 1ba257c3..b96db2d0 100644 --- a/packages/eslint-config/src/config/mixin/node.ts +++ b/packages/eslint-config/src/config/mixin/node.ts @@ -1,6 +1,7 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import n from 'eslint-plugin-n'; import globals from 'globals'; + +import { pickPlugins } from '../../utils.js'; /** * Common rules for JavaScript files. */ @@ -12,9 +13,7 @@ const config: TSESLint.FlatConfig.ConfigArray = [ }, }, - plugins: { - n, - }, + plugins: pickPlugins(['n']), rules: { // require all requires be top-level diff --git a/packages/eslint-config/src/config/mixin/react.ts b/packages/eslint-config/src/config/mixin/react.ts index 5cb7d7f1..f077703f 100644 --- a/packages/eslint-config/src/config/mixin/react.ts +++ b/packages/eslint-config/src/config/mixin/react.ts @@ -1,11 +1,8 @@ import eslintPluginReact from '@eslint-react/eslint-plugin'; -import eslintPluginRightcapital from '@rightcapital/eslint-plugin'; -import eslintPluginStylistic from '@stylistic/eslint-plugin'; import type { TSESLint } from '@typescript-eslint/utils'; -import eslintPluginA11y from 'eslint-plugin-jsx-a11y'; import globals from 'globals'; -import eslintPluginReactHooks from '../../plugins/eslint-plugin-react-hooks.js'; +import { pickPlugins } from '../../utils.js'; /** * Common rules for React, working with TypeScript. */ @@ -17,13 +14,15 @@ const config: TSESLint.FlatConfig.ConfigArray = [ ...globals.browser, }, }, - plugins: { - ...eslintPluginReact.configs.all.plugins, - '@rightcapital': eslintPluginRightcapital, - '@stylistic': eslintPluginStylistic as TSESLint.FlatConfig.Plugin, - 'react-hooks': eslintPluginReactHooks, - 'jsx-a11y': eslintPluginA11y, - }, + plugins: pickPlugins([ + ...(Object.keys( + eslintPluginReact.configs.all.plugins, + ) as (keyof typeof eslintPluginReact.configs.all.plugins)[]), + '@rightcapital', + '@stylistic', + 'react-hooks', + 'jsx-a11y', + ]), rules: { // naming convention '@eslint-react/naming-convention/component-name': ['error', 'PascalCase'], diff --git a/packages/eslint-config/src/config/typescript.ts b/packages/eslint-config/src/config/typescript.ts index bd09d96c..465dc669 100644 --- a/packages/eslint-config/src/config/typescript.ts +++ b/packages/eslint-config/src/config/typescript.ts @@ -1,8 +1,7 @@ -import eslintPluginRightcapital from '@rightcapital/eslint-plugin'; import type { TSESLint } from '@typescript-eslint/utils'; import * as typescriptEslint from 'typescript-eslint'; -import eslintPluginImportX from '../plugins/eslint-plugin-import-x.js'; +import { pickPlugins } from '../utils.js'; import baseConfig from './base/index.js'; /** @@ -12,11 +11,7 @@ const config: TSESLint.FlatConfig.ConfigArray = [ ...baseConfig, ...typescriptEslint.configs.recommendedTypeChecked, { - plugins: { - '@typescript-eslint': typescriptEslint.plugin, - '@rightcapital': eslintPluginRightcapital, - 'import-x': eslintPluginImportX, - }, + plugins: pickPlugins(['@typescript-eslint', '@rightcapital', 'import-x']), languageOptions: { parser: typescriptEslint.parser, parserOptions: { diff --git a/packages/eslint-config/src/index.ts b/packages/eslint-config/src/index.ts index 57ed697c..30bf3fb4 100644 --- a/packages/eslint-config/src/index.ts +++ b/packages/eslint-config/src/index.ts @@ -1,6 +1,4 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import globals from 'globals'; -import { config } from 'typescript-eslint'; import jsConfig from './config/javascript.js'; import linterConfig from './config/linter.js'; @@ -8,25 +6,22 @@ import nodeConfig from './config/mixin/node.js'; import reactConfig from './config/mixin/react.js'; import scriptConfig from './config/mixin/script.js'; import tsConfig from './config/typescript.js'; +import utils from './utils.js'; -const recommendedConfig = config( +const recommendedConfig = utils.defineConfig( ...linterConfig, - { files: ['**/*.{js,cjs,mjs,jsx}'], extends: [...jsConfig], }, - { files: ['**/*.{ts,cts,mts,tsx}'], extends: [...tsConfig], }, - { files: ['**/*.tsx'], extends: [...reactConfig], }, - { // test files files: [ @@ -46,7 +41,6 @@ const recommendedConfig = config( ], }, }, - { // scripts files: [ @@ -60,7 +54,6 @@ const recommendedConfig = config( ], extends: [...scriptConfig], }, - { files: ['*.{js,cjs,mjs,ts,cts,mts}'], // files in the root directory, typically work in node environment extends: [...nodeConfig], @@ -76,15 +69,5 @@ const configs = { script: scriptConfig, } as const satisfies Record; -const utils = { - /** - * Utility function for composing configs from `typescript-eslint`. - * - * @see https://typescript-eslint.io/packages/typescript-eslint#config - */ - config, - globals, -} as const; - export { configs, utils }; export default { configs, utils }; diff --git a/packages/eslint-config/src/plugins/index.ts b/packages/eslint-config/src/plugins/index.ts new file mode 100644 index 00000000..9d20043a --- /dev/null +++ b/packages/eslint-config/src/plugins/index.ts @@ -0,0 +1,30 @@ +import eslintPluginReact from '@eslint-react/eslint-plugin'; +import eslintPluginRightcapital from '@rightcapital/eslint-plugin'; +import eslintPluginStylistic from '@stylistic/eslint-plugin'; +import type { TSESLint } from '@typescript-eslint/utils'; +import eslintPluginA11y from 'eslint-plugin-jsx-a11y'; +import eslintPluginLodash from 'eslint-plugin-lodash'; +import n from 'eslint-plugin-n'; +import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; +import eslintPluginUnicorn from 'eslint-plugin-unicorn'; +import * as typescriptEslint from 'typescript-eslint'; + +import eslintPluginImportX from './eslint-plugin-import-x.js'; +import eslintPluginReactHooks from './eslint-plugin-react-hooks.js'; + +/** + * All plugins used in `@rightcapital/eslint-config`. + */ +export const plugins = { + '@typescript-eslint': typescriptEslint.plugin, + '@rightcapital': eslintPluginRightcapital, + 'import-x': eslintPluginImportX, + 'simple-import-sort': eslintPluginSimpleImportSort, + n, + ...eslintPluginReact.configs.all.plugins, + '@stylistic': eslintPluginStylistic as TSESLint.FlatConfig.Plugin, + 'react-hooks': eslintPluginReactHooks, + 'jsx-a11y': eslintPluginA11y, + lodash: eslintPluginLodash, + unicorn: eslintPluginUnicorn, +}; diff --git a/packages/eslint-config/src/utils.ts b/packages/eslint-config/src/utils.ts new file mode 100644 index 00000000..900691cd --- /dev/null +++ b/packages/eslint-config/src/utils.ts @@ -0,0 +1,57 @@ +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +import { plugins } from './plugins/index.js'; + +/** + * Generate a plugins object from a list of ESLint plugin names + * (Only plugins that are known to `@rightcapital/eslint-config`). + * + * @see {@link plugins} for the list of plugins. + */ +export function pickPlugins(pluginNames?: Array) { + if (!pluginNames) { + return plugins; + } + + return Object.fromEntries( + pluginNames.map((pluginName) => [pluginName, plugins[pluginName]]), + ); +} + +const defineConfig: typeof tseslint.config = (...configs) => + tseslint.config(...configs).map((config) => { + const knownPluginNames = Object.keys(plugins).filter((pluginName) => + Object.keys(config.rules ?? {}).some((rule) => + rule.startsWith(`${pluginName}/`), + ), + ) as Array; + const resolvedPlugins = { + ...pickPlugins(knownPluginNames), + ...config.plugins, + }; + return { + ...(Object.keys(resolvedPlugins).length > 0 + ? { plugins: resolvedPlugins } + : null), + ...config, + }; + }); + +const utils = { + /** + * Utility function for easily composing configs. + * + * This is a wrapper around `typescript-eslint`'s `config` function. + * + * With automatic plugin inference(if the plugin is known to `@rightcapital/eslint-config`). + * + * @see https://typescript-eslint.io/packages/typescript-eslint#config + */ + defineConfig, + globals, + plugins, + pickPlugins, +} as const; + +export default utils; diff --git a/specs/eslint-configs/eslint.config.mjs b/specs/eslint-configs/eslint.config.mjs index 2d80c06a..ce4fe21c 100644 --- a/specs/eslint-configs/eslint.config.mjs +++ b/specs/eslint-configs/eslint.config.mjs @@ -1,8 +1,8 @@ import eslintConfigRightcapital from '@rightcapital/eslint-config'; -const { config } = eslintConfigRightcapital.utils; +const { defineConfig } = eslintConfigRightcapital.utils; -export default config( +export default defineConfig( { files: ['src/javascript.js'], extends: [...eslintConfigRightcapital.configs.js],