From 3e2ae169adcc58cc4b9221fc68c924d5aec575b3 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 4 Aug 2024 08:35:58 -0500 Subject: [PATCH] feat: add flat config support This change adds support for ESLint's new Flat config system. It maintains backwards compatibility with eslintrc style configs as well. To achieve this, we're now dynamically creating flat configs on a new `flatConfigs` export. I was a bit on the fence about using this convention, or the other convention that's become prevalent in the community: adding the flat configs directly to the `configs` object, but with a 'flat/' prefix. I like this better, since it's slightly more ergonomic when using it in practice. e.g. `...importX.flatConfigs.recommended` vs `...importX.configs['flat/recommended']`, but i'm open to changing that. Example Usage ```js import importPlugin from 'eslint-plugin-import'; import js from '@eslint/js'; import tsParser from '@typescript-eslint/parser'; export default [ js.configs.recommended, importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.react, importPlugin.flatConfigs.typescript, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], languageOptions: { parser: tsParser, ecmaVersion: 'latest', sourceType: 'module', }, ignores: ['eslint.config.js'], rules: { 'no-unused-vars': 'off', 'import/no-dynamic-require': 'warn', 'import/no-nodejs-modules': 'warn', }, }, ]; ``` Closes #29 --- .changeset/great-dodos-dream.md | 5 ++ package.json | 2 +- src/config/flat/electron.ts | 10 ++++ src/config/flat/errors.ts | 15 ++++++ src/config/flat/react-native.ts | 15 ++++++ src/config/flat/react.ts | 21 ++++++++ src/config/flat/recommended.ts | 27 ++++++++++ src/config/flat/stage-0.ts | 12 +++++ src/config/flat/typescript.ts | 41 ++++++++++++++++ src/config/flat/warnings.ts | 12 +++++ src/config/typescript.ts | 12 +++-- src/index.ts | 87 ++++++++++++++++++++++++++------- src/types.ts | 9 ++++ src/utils/ignore.ts | 2 +- 14 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 .changeset/great-dodos-dream.md create mode 100644 src/config/flat/electron.ts create mode 100644 src/config/flat/errors.ts create mode 100644 src/config/flat/react-native.ts create mode 100644 src/config/flat/react.ts create mode 100644 src/config/flat/recommended.ts create mode 100644 src/config/flat/stage-0.ts create mode 100644 src/config/flat/typescript.ts create mode 100644 src/config/flat/warnings.ts diff --git a/.changeset/great-dodos-dream.md b/.changeset/great-dodos-dream.md new file mode 100644 index 0000000000..d073e2016f --- /dev/null +++ b/.changeset/great-dodos-dream.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": minor +--- + +add support for flat configs diff --git a/package.json b/package.json index fedc326ea4..a960cc74de 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "codesandbox:install": "yarn --ignore-engines", "lint": "run-p lint:*", "lint:docs": "yarn update:eslint-docs --check", - "lint:es": "ESLINT_USE_FLAT_CONFIG=false eslint . --cache", + "lint:es": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --cache", "lint:tsc": "tsc -p tsconfig.base.json --noEmit", "prepare": "patch-package", "release": "changeset publish", diff --git a/src/config/flat/electron.ts b/src/config/flat/electron.ts new file mode 100644 index 0000000000..add2e58d98 --- /dev/null +++ b/src/config/flat/electron.ts @@ -0,0 +1,10 @@ +import type { PluginFlatConfig } from '../../types' + +/** + * Default settings for Electron applications. + */ +export default { + settings: { + 'import-x/core-modules': ['electron'], + }, +} satisfies PluginFlatConfig diff --git a/src/config/flat/errors.ts b/src/config/flat/errors.ts new file mode 100644 index 0000000000..a27a60153c --- /dev/null +++ b/src/config/flat/errors.ts @@ -0,0 +1,15 @@ +import type { PluginFlatConfig } from '../../types' + +/** + * unopinionated config. just the things that are necessarily runtime errors + * waiting to happen. + */ +export default { + rules: { + 'import-x/no-unresolved': 2, + 'import-x/named': 2, + 'import-x/namespace': 2, + 'import-x/default': 2, + 'import-x/export': 2, + }, +} satisfies PluginFlatConfig diff --git a/src/config/flat/react-native.ts b/src/config/flat/react-native.ts new file mode 100644 index 0000000000..5f924dec6d --- /dev/null +++ b/src/config/flat/react-native.ts @@ -0,0 +1,15 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * adds platform extensions to Node resolver + */ +export default { + settings: { + 'import-x/resolver': { + node: { + // Note: will not complain if only _one_ of these files exists. + extensions: ['.js', '.web.js', '.ios.js', '.android.js'], + }, + }, + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/flat/react.ts b/src/config/flat/react.ts new file mode 100644 index 0000000000..dd688e3cc7 --- /dev/null +++ b/src/config/flat/react.ts @@ -0,0 +1,21 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * Adds `.jsx` as an extension, and enables JSX parsing. + * + * Even if _you_ aren't using JSX (or .jsx) directly, if your dependencies + * define jsnext:main and have JSX internally, you may run into problems + * if you don't enable these settings at the top level. + */ +export default { + settings: { + 'import-x/extensions': ['.js', '.jsx', '.mjs', '.cjs'], + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/flat/recommended.ts b/src/config/flat/recommended.ts new file mode 100644 index 0000000000..69056e74ae --- /dev/null +++ b/src/config/flat/recommended.ts @@ -0,0 +1,27 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * The basics. + */ +export default { + rules: { + // analysis/correctness + 'import-x/no-unresolved': 'error', + 'import-x/named': 'error', + 'import-x/namespace': 'error', + 'import-x/default': 'error', + 'import-x/export': 'error', + + // red flags (thus, warnings) + 'import-x/no-named-as-default': 'warn', + 'import-x/no-named-as-default-member': 'warn', + 'import-x/no-duplicates': 'warn', + }, + + // need all these for parsing dependencies (even if _your_ code doesn't need + // all of them) + languageOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/flat/stage-0.ts b/src/config/flat/stage-0.ts new file mode 100644 index 0000000000..ca7b0be16b --- /dev/null +++ b/src/config/flat/stage-0.ts @@ -0,0 +1,12 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * Rules in progress. + * + * Do not expect these to adhere to semver across releases. + */ +export default { + rules: { + 'import-x/no-deprecated': 1, + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/flat/typescript.ts b/src/config/flat/typescript.ts new file mode 100644 index 0000000000..a7484bfd9e --- /dev/null +++ b/src/config/flat/typescript.ts @@ -0,0 +1,41 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * This config: + * 1) adds `.jsx`, `.ts`, `.cts`, `.mts`, and `.tsx` as an extension + * 2) enables JSX/TSX parsing + */ + +// Omit `.d.ts` because 1) TypeScript compilation already confirms that +// types are resolved, and 2) it would mask an unresolved +// `.ts`/`.tsx`/`.js`/`.jsx` implementation. +const typeScriptExtensions = ['.ts', '.tsx', '.cts', '.mts'] as const + +const allExtensions = [ + ...typeScriptExtensions, + '.js', + '.jsx', + '.cjs', + '.mjs', +] as const + +export default { + settings: { + 'import-x/extensions': allExtensions, + 'import-x/external-module-folders': ['node_modules', 'node_modules/@types'], + 'import-x/parsers': { + '@typescript-eslint/parser': [...typeScriptExtensions], + }, + 'import-x/resolver': { + node: { + extensions: allExtensions, + }, + }, + }, + rules: { + // analysis/correctness + + // TypeScript compilation already ensures that named imports exist in the referenced module + 'import-x/named': 'off', + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/flat/warnings.ts b/src/config/flat/warnings.ts new file mode 100644 index 0000000000..ca9dde2db4 --- /dev/null +++ b/src/config/flat/warnings.ts @@ -0,0 +1,12 @@ +import type { PluginFlatBaseConfig } from '../../types' + +/** + * more opinionated config. + */ +export default { + rules: { + 'import-x/no-named-as-default': 1, + 'import-x/no-named-as-default-member': 1, + 'import-x/no-duplicates': 1, + }, +} satisfies PluginFlatBaseConfig diff --git a/src/config/typescript.ts b/src/config/typescript.ts index 21269b2b45..444fee65ce 100644 --- a/src/config/typescript.ts +++ b/src/config/typescript.ts @@ -9,16 +9,22 @@ import type { PluginConfig } from '../types' // Omit `.d.ts` because 1) TypeScript compilation already confirms that // types are resolved, and 2) it would mask an unresolved // `.ts`/`.tsx`/`.js`/`.jsx` implementation. -const typeScriptExtensions = ['.ts', '.tsx'] as const +const typeScriptExtensions = ['.ts', '.tsx', '.cts', '.mts'] as const -const allExtensions = [...typeScriptExtensions, '.js', '.jsx'] as const +const allExtensions = [ + ...typeScriptExtensions, + '.js', + '.jsx', + '.cjs', + '.mjs', +] as const export = { settings: { 'import-x/extensions': allExtensions, 'import-x/external-module-folders': ['node_modules', 'node_modules/@types'], 'import-x/parsers': { - '@typescript-eslint/parser': [...typeScriptExtensions, '.cts', '.mts'], + '@typescript-eslint/parser': [...typeScriptExtensions], }, 'import-x/resolver': { node: { diff --git a/src/index.ts b/src/index.ts index 69cd2bfd4b..8c84469d3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,28 @@ import type { TSESLint } from '@typescript-eslint/utils' -// rules +import { name, version } from '../package.json' + +// legacy configs import electron from './config/electron' import errors from './config/errors' + +// flat configs +import electronFlat from './config/flat/electron' +import errorsFlat from './config/flat/errors' +import reactFlat from './config/flat/react' +import reactNativeFlat from './config/flat/react-native' +import recommendedFlat from './config/flat/recommended' +import stage0Flat from './config/flat/stage-0' +import typescriptFlat from './config/flat/typescript' +import warningsFlat from './config/flat/warnings' import react from './config/react' import reactNative from './config/react-native' import recommended from './config/recommended' import stage0 from './config/stage-0' import typescript from './config/typescript' import warnings from './config/warnings' + +// rules import consistentTypeSpecifierStyle from './rules/consistent-type-specifier-style' import default_ from './rules/default' import dynamicImportChunkname from './rules/dynamic-import-chunkname' @@ -55,23 +69,11 @@ import order from './rules/order' import preferDefaultExport from './rules/prefer-default-export' import unambiguous from './rules/unambiguous' // configs -import type { PluginConfig } from './types' - -const configs = { - recommended, - - errors, - warnings, - - // shhhh... work in progress "secret" rules - 'stage-0': stage0, - - // useful stuff for folks using various environments - react, - 'react-native': reactNative, - electron, - typescript, -} satisfies Record +import type { + PluginConfig, + PluginFlatBaseConfig, + PluginFlatConfig, +} from './types' const rules = { 'no-unresolved': noUnresolved, @@ -129,7 +131,56 @@ const rules = { 'imports-first': importsFirst, } satisfies Record> +const configs = { + recommended, + + errors, + warnings, + + // shhhh... work in progress "secret" rules + 'stage-0': stage0, + + // useful stuff for folks using various environments + react, + 'react-native': reactNative, + electron, + typescript, +} satisfies Record + +// Base Plugin Object +const plugin = { + meta: { name, version }, + rules, +} + +// Create flat configs (Only ones that declare plugins and parser options need to be different from the legacy config) +const createFlatConfig = ( + baseConfig: PluginFlatBaseConfig, + configName: string, +): PluginFlatConfig => ({ + ...baseConfig, + name: `import-x/${configName}`, + plugins: { 'import-x': plugin }, +}) + +const flatConfigs = { + recommended: createFlatConfig(recommendedFlat, 'recommended'), + + errors: createFlatConfig(errorsFlat, 'errors'), + warnings: createFlatConfig(warningsFlat, 'warnings'), + + // shhhh... work in progress "secret" rules + 'stage-0': createFlatConfig(stage0Flat, 'stage-0'), + + // useful stuff for folks using various environments + react: reactFlat, + 'react-native': reactNativeFlat, + electron: electronFlat, + typescript: typescriptFlat, +} satisfies Record + export = { configs, + flatConfigs, rules, } diff --git a/src/types.ts b/src/types.ts index 34dd04fb3c..5a0c88d4bd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,6 +70,15 @@ export type PluginConfig = { rules?: Record<`${PluginName}/${string}`, TSESLint.Linter.RuleEntry> } & TSESLint.Linter.ConfigType +export type PluginFlatBaseConfig = { + settings?: PluginSettings + rules?: Record<`${PluginName}/${string}`, TSESLint.FlatConfig.RuleEntry> +} & TSESLint.FlatConfig.Config + +export type PluginFlatConfig = PluginFlatBaseConfig & { + name?: `${PluginName}/${string}` +} + export type RuleContext< TMessageIds extends string = string, TOptions extends readonly unknown[] = readonly unknown[], diff --git a/src/utils/ignore.ts b/src/utils/ignore.ts index e96575ee8b..105357a1a9 100644 --- a/src/utils/ignore.ts +++ b/src/utils/ignore.ts @@ -28,7 +28,7 @@ function validExtensions(context: ChildContext | RuleContext) { export function getFileExtensions(settings: PluginSettings) { // start with explicit JS-parsed extensions const exts = new Set( - settings['import-x/extensions'] || ['.js'], + settings['import-x/extensions'] || ['.js', '.mjs', '.cjs'], ) // all alternate parser extensions are also valid