Skip to content

Commit

Permalink
feat: add flat config support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
michaelfaith committed Aug 18, 2024
1 parent 4ac2b98 commit cb36e4b
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-dodos-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-import-x": minor
---

add support for flat configs
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions src/config/flat/electron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PluginFlatConfig } from '../../types'

/**
* Default settings for Electron applications.
*/
export default {
settings: {
'import-x/core-modules': ['electron'],
},
} satisfies PluginFlatConfig
15 changes: 15 additions & 0 deletions src/config/flat/errors.ts
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions src/config/flat/react-native.ts
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions src/config/flat/react.ts
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions src/config/flat/recommended.ts
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/config/flat/stage-0.ts
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions src/config/flat/typescript.ts
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/config/flat/warnings.ts
Original file line number Diff line number Diff line change
@@ -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
12 changes: 9 additions & 3 deletions src/config/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
84 changes: 66 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import type { TSESLint } from '@typescript-eslint/utils'

// rules
import { name, version } from '../package.json'

import electron from './config/electron'
import errors from './config/errors'
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'

Check failure on line 20 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Test with Node.js 20 and ESLint 8.56 on ubuntu-latest

There should be no empty line within import group

Check failure on line 20 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on ubuntu-latest

There should be no empty line within import group

Check failure on line 20 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Test with Node.js 20 and ESLint 9 on ubuntu-latest

There should be no empty line within import group

// rules
import consistentTypeSpecifierStyle from './rules/consistent-type-specifier-style'
import default_ from './rules/default'
import dynamicImportChunkname from './rules/dynamic-import-chunkname'
Expand Down Expand Up @@ -55,23 +66,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<string, PluginConfig>
import type {
PluginConfig,
PluginFlatBaseConfig,
PluginFlatConfig,
} from './types'

const rules = {
'no-unresolved': noUnresolved,
Expand Down Expand Up @@ -129,7 +128,56 @@ const rules = {
'imports-first': importsFirst,
} satisfies Record<string, TSESLint.RuleModule<string, readonly unknown[]>>

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<string, PluginConfig>

// 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<string, PluginFlatConfig>

export = {
configs,
flatConfigs,
rules,
}
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
2 changes: 1 addition & 1 deletion src/utils/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function validExtensions(context: ChildContext | RuleContext) {
export function getFileExtensions(settings: PluginSettings) {
// start with explicit JS-parsed extensions
const exts = new Set<FileExtension>(
settings['import-x/extensions'] || ['.js'],
settings['import-x/extensions'] || ['.js', '.mjs', '.cjs'],
)

// all alternate parser extensions are also valid
Expand Down

0 comments on commit cb36e4b

Please sign in to comment.