Skip to content

Commit 806e3c2

Browse files
michaelfaithljharb
authored andcommittedJun 18, 2024··
[New] add support for Flat Config
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. 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', }, }, ]; ```
1 parent b340f1f commit 806e3c2

29 files changed

+541
-49
lines changed
 

‎.editorconfig

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ insert_final_newline = true
77
indent_style = space
88
indent_size = 2
99
end_of_line = lf
10+
quote_type = single

‎.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ tests/files/with-syntax-error
77
tests/files/just-json-files/invalid.json
88
tests/files/typescript-d-ts/
99
resolvers/webpack/test/files
10+
examples
1011
# we want to ignore "tests/files" here, but unfortunately doing so would
1112
# interfere with unit test and fail it for some reason.
1213
# tests/files

‎CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1010
- [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian])
1111
- [`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode ([#3004], thanks [@amsardesai])
1212
- [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind])
13+
- add support for Flat Config ([#3018], thanks [@michaelfaith])
1314

1415
### Fixed
1516
- [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb])
@@ -1125,6 +1126,7 @@ for info on changes for earlier releases.
11251126

11261127
[#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036
11271128
[#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033
1129+
[#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018
11281130
[#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012
11291131
[#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011
11301132
[#3004]: https://github.com/import-js/eslint-plugin-import/pull/3004
@@ -1874,6 +1876,7 @@ for info on changes for earlier releases.
18741876
[@meowtec]: https://github.com/meowtec
18751877
[@mgwalker]: https://github.com/mgwalker
18761878
[@mhmadhamster]: https://github.com/MhMadHamster
1879+
[@michaelfaith]: https://github.com/michaelfaith
18771880
[@MikeyBeLike]: https://github.com/MikeyBeLike
18781881
[@minervabot]: https://github.com/minervabot
18791882
[@mpint]: https://github.com/mpint

‎README.md

+30-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ The maintainers of `eslint-plugin-import` and thousands of other packages are wo
106106
npm install eslint-plugin-import --save-dev
107107
```
108108

109+
### Config - Legacy (`.eslintrc`)
110+
109111
All rules are off by default. However, you may configure them manually
110112
in your `.eslintrc.(yml|json|js)`, or extend one of the canned configs:
111113

@@ -123,14 +125,41 @@ plugins:
123125
- import
124126

125127
rules:
126-
import/no-unresolved: [2, {commonjs: true, amd: true}]
128+
import/no-unresolved: [2, { commonjs: true, amd: true }]
127129
import/named: 2
128130
import/namespace: 2
129131
import/default: 2
130132
import/export: 2
131133
# etc...
132134
```
133135

136+
### Config - Flat (`eslint.config.js`)
137+
138+
All rules are off by default. However, you may configure them manually
139+
in your `eslint.config.(js|cjs|mjs)`, or extend one of the canned configs:
140+
141+
```js
142+
import importPlugin from 'eslint-plugin-import';
143+
import js from '@eslint/js';
144+
145+
export default [
146+
js.configs.recommended,
147+
importPlugin.flatConfigs.recommended,
148+
{
149+
files: ['**/*.{js,mjs,cjs}'],
150+
languageOptions: {
151+
ecmaVersion: 'latest',
152+
sourceType: 'module',
153+
},
154+
rules: {
155+
'no-unused-vars': 'off',
156+
'import/no-dynamic-require': 'warn',
157+
'import/no-nodejs-modules': 'warn',
158+
},
159+
},
160+
];
161+
```
162+
134163
## TypeScript
135164

136165
You may use the following snippet or assemble your own config using the granular settings described below it.

‎config/flat/errors.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* unopinionated config. just the things that are necessarily runtime errors
3+
* waiting to happen.
4+
* @type {Object}
5+
*/
6+
module.exports = {
7+
rules: {
8+
'import/no-unresolved': 2,
9+
'import/named': 2,
10+
'import/namespace': 2,
11+
'import/default': 2,
12+
'import/export': 2,
13+
},
14+
};

‎config/flat/react.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Adds `.jsx` as an extension, and enables JSX parsing.
3+
*
4+
* Even if _you_ aren't using JSX (or .jsx) directly, if your dependencies
5+
* define jsnext:main and have JSX internally, you may run into problems
6+
* if you don't enable these settings at the top level.
7+
*/
8+
module.exports = {
9+
settings: {
10+
'import/extensions': ['.js', '.jsx', '.mjs', '.cjs'],
11+
},
12+
languageOptions: {
13+
parserOptions: {
14+
ecmaFeatures: {
15+
jsx: true,
16+
},
17+
},
18+
},
19+
};

‎config/flat/recommended.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* The basics.
3+
* @type {Object}
4+
*/
5+
module.exports = {
6+
rules: {
7+
// analysis/correctness
8+
'import/no-unresolved': 'error',
9+
'import/named': 'error',
10+
'import/namespace': 'error',
11+
'import/default': 'error',
12+
'import/export': 'error',
13+
14+
// red flags (thus, warnings)
15+
'import/no-named-as-default': 'warn',
16+
'import/no-named-as-default-member': 'warn',
17+
'import/no-duplicates': 'warn',
18+
},
19+
20+
// need all these for parsing dependencies (even if _your_ code doesn't need
21+
// all of them)
22+
languageOptions: {
23+
ecmaVersion: 2018,
24+
sourceType: 'module',
25+
},
26+
};

‎config/flat/warnings.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* more opinionated config.
3+
* @type {Object}
4+
*/
5+
module.exports = {
6+
rules: {
7+
'import/no-named-as-default': 1,
8+
'import/no-named-as-default-member': 1,
9+
'import/no-duplicates': 1,
10+
},
11+
};

‎config/react.js

-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
* if you don't enable these settings at the top level.
77
*/
88
module.exports = {
9-
109
settings: {
1110
'import/extensions': ['.js', '.jsx'],
1211
},
1312

1413
parserOptions: {
1514
ecmaFeatures: { jsx: true },
1615
},
17-
1816
};

‎config/typescript.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// `.ts`/`.tsx`/`.js`/`.jsx` implementation.
1010
const typeScriptExtensions = ['.ts', '.cts', '.mts', '.tsx'];
1111

12-
const allExtensions = [...typeScriptExtensions, '.js', '.jsx'];
12+
const allExtensions = [...typeScriptExtensions, '.js', '.jsx', '.mjs', '.cjs'];
1313

1414
module.exports = {
1515
settings: {

‎examples/flat/eslint.config.mjs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import importPlugin from 'eslint-plugin-import';
2+
import js from '@eslint/js';
3+
import tsParser from '@typescript-eslint/parser';
4+
5+
export default [
6+
js.configs.recommended,
7+
importPlugin.flatConfigs.recommended,
8+
importPlugin.flatConfigs.react,
9+
importPlugin.flatConfigs.typescript,
10+
{
11+
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
12+
languageOptions: {
13+
parser: tsParser,
14+
ecmaVersion: 'latest',
15+
sourceType: 'module',
16+
},
17+
ignores: ['eslint.config.mjs', '**/exports-unused.ts'],
18+
rules: {
19+
'no-unused-vars': 'off',
20+
'import/no-dynamic-require': 'warn',
21+
'import/no-nodejs-modules': 'warn',
22+
'import/no-unused-modules': ['warn', { unusedExports: true }],
23+
},
24+
},
25+
];

‎examples/flat/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "flat",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint src --report-unused-disable-directives"
7+
},
8+
"devDependencies": {
9+
"@eslint/js": "^9.5.0",
10+
"@types/node": "^20.14.5",
11+
"@typescript-eslint/parser": "^7.13.1",
12+
"cross-env": "^7.0.3",
13+
"eslint": "^8.57.0",
14+
"eslint-plugin-import": "file:../..",
15+
"typescript": "^5.4.5"
16+
}
17+
}

‎examples/flat/src/exports-unused.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

‎examples/flat/src/exports.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

‎examples/flat/src/imports.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//import c from './exports';
2+
import { a, b } from './exports';
3+
import type { ScalarType, ObjType } from './exports';
4+
5+
import path from 'path';
6+
import fs from 'node:fs';
7+
import console from 'console';

‎examples/flat/src/jsx.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Components = () => {
2+
return <></>;
3+
};

‎examples/flat/tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react-jsx",
4+
"lib": ["ESNext"],
5+
"target": "ESNext",
6+
"module": "ESNext",
7+
"rootDir": "./",
8+
"moduleResolution": "Bundler",
9+
"esModuleInterop": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"skipLibCheck": true
13+
}
14+
}

‎examples/legacy/.eslintrc.cjs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
root: true,
3+
env: { es2022: true },
4+
extends: [
5+
'eslint:recommended',
6+
'plugin:import/recommended',
7+
'plugin:import/react',
8+
'plugin:import/typescript',
9+
],
10+
settings: {},
11+
ignorePatterns: ['.eslintrc.cjs', '**/exports-unused.ts'],
12+
parser: '@typescript-eslint/parser',
13+
parserOptions: {
14+
ecmaVersion: 'latest',
15+
sourceType: 'module',
16+
},
17+
plugins: ['import'],
18+
rules: {
19+
'no-unused-vars': 'off',
20+
'import/no-dynamic-require': 'warn',
21+
'import/no-nodejs-modules': 'warn',
22+
'import/no-unused-modules': ['warn', { unusedExports: true }],
23+
},
24+
};

‎examples/legacy/package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "legacy",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src --ext js,jsx,ts,tsx --report-unused-disable-directives"
7+
},
8+
"devDependencies": {
9+
"@types/node": "^20.14.5",
10+
"@typescript-eslint/parser": "^7.13.1",
11+
"cross-env": "^7.0.3",
12+
"eslint": "^8.57.0",
13+
"eslint-plugin-import": "file:../..",
14+
"typescript": "^5.4.5"
15+
}
16+
}

‎examples/legacy/src/exports-unused.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

‎examples/legacy/src/exports.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type ScalarType = string | number;
2+
export type ObjType = {
3+
a: ScalarType;
4+
b: ScalarType;
5+
};
6+
7+
export const a = 13;
8+
export const b = 18;
9+
10+
const defaultExport: ObjType = { a, b };
11+
12+
export default defaultExport;

‎examples/legacy/src/imports.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//import c from './exports';
2+
import { a, b } from './exports';
3+
import type { ScalarType, ObjType } from './exports';
4+
5+
import path from 'path';
6+
import fs from 'node:fs';
7+
import console from 'console';

‎examples/legacy/src/jsx.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Components = () => {
2+
return <></>;
3+
};

‎examples/legacy/tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react-jsx",
4+
"lib": ["ESNext"],
5+
"target": "ESNext",
6+
"module": "ESNext",
7+
"rootDir": "./",
8+
"moduleResolution": "Bundler",
9+
"esModuleInterop": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"skipLibCheck": true
13+
}
14+
}

‎package.json

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"test": "npm run tests-only",
3232
"test-compiled": "npm run prepublish && BABEL_ENV=testCompiled mocha --compilers js:babel-register tests/src",
3333
"test-all": "node --require babel-register ./scripts/testAll",
34+
"test-examples": "npm run build && npm run test-example:legacy && npm run test-example:flat",
35+
"test-example:legacy": "cd examples/legacy && npm install && npm run lint",
36+
"test-example:flat": "cd examples/flat && npm install && npm run lint",
3437
"prepublishOnly": "safe-publish-latest && npm run build",
3538
"prepublish": "not-in-publish || npm run prepublishOnly",
3639
"preupdate:eslint-docs": "npm run build",

‎src/core/fsWalk.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* This is intended to provide similar capability as the sync api from @nodelib/fs.walk, until `eslint-plugin-import`
3+
* is willing to modernize and update their minimum node version to at least v16. I intentionally made the
4+
* shape of the API (for the part we're using) the same as @nodelib/fs.walk so that that can be swapped in
5+
* when the repo is ready for it.
6+
*/
7+
8+
import path from 'path';
9+
import { readdirSync } from 'fs';
10+
11+
/** @typedef {{ name: string, path: string, dirent: import('fs').Dirent }} Entry */
12+
13+
/**
14+
* Do a comprehensive walk of the provided src directory, and collect all entries. Filter out
15+
* any directories or entries using the optional filter functions.
16+
* @param {string} root - path to the root of the folder we're walking
17+
* @param {{ deepFilter?: (entry: Entry) => boolean, entryFilter?: (entry: Entry) => boolean }} options
18+
* @param {Entry} currentEntry - entry for the current directory we're working in
19+
* @param {Entry[]} existingEntries - list of all entries so far
20+
* @returns {Entry[]} an array of directory entries
21+
*/
22+
export function walkSync(root, options, currentEntry, existingEntries) {
23+
// Extract the filter functions. Default to evaluating true, if no filter passed in.
24+
const { deepFilter = () => true, entryFilter = () => true } = options;
25+
26+
let entryList = existingEntries || [];
27+
const currentRelativePath = currentEntry ? currentEntry.path : '.';
28+
const fullPath = currentEntry ? path.join(root, currentEntry.path) : root;
29+
30+
const dirents = readdirSync(fullPath, { withFileTypes: true });
31+
dirents.forEach((dirent) => {
32+
/** @type {Entry} */
33+
const entry = {
34+
name: dirent.name,
35+
path: path.join(currentRelativePath, dirent.name),
36+
dirent,
37+
};
38+
39+
if (dirent.isDirectory() && deepFilter(entry)) {
40+
entryList.push(entry);
41+
entryList = walkSync(root, options, entry, entryList);
42+
} else if (dirent.isFile() && entryFilter(entry)) {
43+
entryList.push(entry);
44+
}
45+
});
46+
47+
return entryList;
48+
}

‎src/index.js

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { name, version } from '../package.json';
2+
13
export const rules = {
24
'no-unresolved': require('./rules/no-unresolved'),
35
named: require('./rules/named'),
@@ -69,3 +71,32 @@ export const configs = {
6971
electron: require('../config/electron'),
7072
typescript: require('../config/typescript'),
7173
};
74+
75+
// Base Plugin Object
76+
const importPlugin = {
77+
meta: { name, version },
78+
rules,
79+
};
80+
81+
// Create flat configs (Only ones that declare plugins and parser options need to be different from the legacy config)
82+
const createFlatConfig = (baseConfig, configName) => ({
83+
...baseConfig,
84+
name: `import/${configName}`,
85+
plugins: { import: importPlugin },
86+
});
87+
88+
export const flatConfigs = {
89+
recommended: createFlatConfig(
90+
require('../config/flat/recommended'),
91+
'recommended',
92+
),
93+
94+
errors: createFlatConfig(require('../config/flat/errors'), 'errors'),
95+
warnings: createFlatConfig(require('../config/flat/warnings'), 'warnings'),
96+
97+
// useful stuff for folks using various environments
98+
react: require('../config/flat/react'),
99+
'react-native': configs['react-native'],
100+
electron: configs.electron,
101+
typescript: configs.typescript,
102+
};

‎src/rules/no-unused-modules.js

+168-42
Original file line numberDiff line numberDiff line change
@@ -7,61 +7,177 @@
77
import { getFileExtensions } from 'eslint-module-utils/ignore';
88
import resolve from 'eslint-module-utils/resolve';
99
import visit from 'eslint-module-utils/visit';
10-
import { dirname, join } from 'path';
10+
import { dirname, join, resolve as resolvePath } from 'path';
1111
import readPkgUp from 'eslint-module-utils/readPkgUp';
1212
import values from 'object.values';
1313
import includes from 'array-includes';
1414
import flatMap from 'array.prototype.flatmap';
1515

16+
import { walkSync } from '../core/fsWalk';
1617
import ExportMapBuilder from '../exportMap/builder';
1718
import recursivePatternCapture from '../exportMap/patternCapture';
1819
import docsUrl from '../docsUrl';
1920

20-
let FileEnumerator;
21-
let listFilesToProcess;
21+
/**
22+
* Attempt to load the internal `FileEnumerator` class, which has existed in a couple
23+
* of different places, depending on the version of `eslint`. Try requiring it from both
24+
* locations.
25+
* @returns Returns the `FileEnumerator` class if its requirable, otherwise `undefined`.
26+
*/
27+
function requireFileEnumerator() {
28+
let FileEnumerator;
2229

23-
try {
24-
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
25-
} catch (e) {
30+
// Try getting it from the eslint private / deprecated api
2631
try {
27-
// has been moved to eslint/lib/cli-engine/file-enumerator in version 6
28-
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
32+
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
2933
} catch (e) {
34+
// Absorb this if it's MODULE_NOT_FOUND
35+
if (e.code !== 'MODULE_NOT_FOUND') {
36+
throw e;
37+
}
38+
39+
// If not there, then try getting it from eslint/lib/cli-engine/file-enumerator (moved there in v6)
3040
try {
31-
// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3
32-
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils');
33-
34-
// Prevent passing invalid options (extensions array) to old versions of the function.
35-
// https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280
36-
// https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269
37-
listFilesToProcess = function (src, extensions) {
38-
return originalListFilesToProcess(src, {
39-
extensions,
40-
});
41-
};
41+
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
4242
} catch (e) {
43-
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util');
43+
// Absorb this if it's MODULE_NOT_FOUND
44+
if (e.code !== 'MODULE_NOT_FOUND') {
45+
throw e;
46+
}
47+
}
48+
}
49+
return FileEnumerator;
50+
}
51+
52+
/**
53+
*
54+
* @param FileEnumerator the `FileEnumerator` class from `eslint`'s internal api
55+
* @param {string} src path to the src root
56+
* @param {string[]} extensions list of supported extensions
57+
* @returns {{ filename: string, ignored: boolean }[]} list of files to operate on
58+
*/
59+
function listFilesUsingFileEnumerator(FileEnumerator, src, extensions) {
60+
const e = new FileEnumerator({
61+
extensions,
62+
});
63+
64+
return Array.from(
65+
e.iterateFiles(src),
66+
({ filePath, ignored }) => ({ filename: filePath, ignored }),
67+
);
68+
}
4469

45-
listFilesToProcess = function (src, extensions) {
46-
const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`)));
70+
/**
71+
* Attempt to require old versions of the file enumeration capability from v6 `eslint` and earlier, and use
72+
* those functions to provide the list of files to operate on
73+
* @param {string} src path to the src root
74+
* @param {string[]} extensions list of supported extensions
75+
* @returns {string[]} list of files to operate on
76+
*/
77+
function listFilesWithLegacyFunctions(src, extensions) {
78+
try {
79+
// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3
80+
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils');
81+
// Prevent passing invalid options (extensions array) to old versions of the function.
82+
// https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280
83+
// https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269
4784

48-
return originalListFilesToProcess(patterns);
49-
};
85+
return originalListFilesToProcess(src, {
86+
extensions,
87+
});
88+
} catch (e) {
89+
// Absorb this if it's MODULE_NOT_FOUND
90+
if (e.code !== 'MODULE_NOT_FOUND') {
91+
throw e;
5092
}
93+
94+
// Last place to try (pre v5.3)
95+
const {
96+
listFilesToProcess: originalListFilesToProcess,
97+
} = require('eslint/lib/util/glob-util');
98+
const patterns = src.concat(
99+
flatMap(
100+
src,
101+
(pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`),
102+
),
103+
);
104+
105+
return originalListFilesToProcess(patterns);
51106
}
52107
}
53108

54-
if (FileEnumerator) {
55-
listFilesToProcess = function (src, extensions) {
56-
const e = new FileEnumerator({
57-
extensions,
109+
/**
110+
* Given a source root and list of supported extensions, use fsWalk and the
111+
* new `eslint` `context.session` api to build the list of files we want to operate on
112+
* @param {string[]} srcPaths array of source paths (for flat config this should just be a singular root (e.g. cwd))
113+
* @param {string[]} extensions list of supported extensions
114+
* @param {{ isDirectoryIgnored: (path: string) => boolean, isFileIgnored: (path: string) => boolean }} session eslint context session object
115+
* @returns {string[]} list of files to operate on
116+
*/
117+
function listFilesWithModernApi(srcPaths, extensions, session) {
118+
/** @type {string[]} */
119+
const files = [];
120+
121+
for (let i = 0; i < srcPaths.length; i++) {
122+
const src = srcPaths[i];
123+
// Use walkSync along with the new session api to gather the list of files
124+
const entries = walkSync(src, {
125+
deepFilter(entry) {
126+
const fullEntryPath = resolvePath(src, entry.path);
127+
128+
// Include the directory if it's not marked as ignore by eslint
129+
return !session.isDirectoryIgnored(fullEntryPath);
130+
},
131+
entryFilter(entry) {
132+
const fullEntryPath = resolvePath(src, entry.path);
133+
134+
// Include the file if it's not marked as ignore by eslint and its extension is included in our list
135+
return (
136+
!session.isFileIgnored(fullEntryPath)
137+
&& extensions.find((extension) => entry.path.endsWith(extension))
138+
);
139+
},
58140
});
59141

60-
return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
61-
ignored,
62-
filename: filePath,
63-
}));
64-
};
142+
// Filter out directories and map entries to their paths
143+
files.push(
144+
...entries
145+
.filter((entry) => !entry.dirent.isDirectory())
146+
.map((entry) => entry.path),
147+
);
148+
}
149+
return files;
150+
}
151+
152+
/**
153+
* Given a src pattern and list of supported extensions, return a list of files to process
154+
* with this rule.
155+
* @param {string} src - file, directory, or glob pattern of files to act on
156+
* @param {string[]} extensions - list of supported file extensions
157+
* @param {import('eslint').Rule.RuleContext} context - the eslint context object
158+
* @returns {string[] | { filename: string, ignored: boolean }[]} the list of files that this rule will evaluate.
159+
*/
160+
function listFilesToProcess(src, extensions, context) {
161+
// If the context object has the new session functions, then prefer those
162+
// Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support.
163+
// https://github.com/eslint/eslint/issues/18087
164+
if (
165+
context.session
166+
&& context.session.isFileIgnored
167+
&& context.session.isDirectoryIgnored
168+
) {
169+
return listFilesWithModernApi(src, extensions, context.session);
170+
}
171+
172+
// Fallback to og FileEnumerator
173+
const FileEnumerator = requireFileEnumerator();
174+
175+
// If we got the FileEnumerator, then let's go with that
176+
if (FileEnumerator) {
177+
return listFilesUsingFileEnumerator(FileEnumerator, src, extensions);
178+
}
179+
// If not, then we can try even older versions of this capability (listFilesToProcess)
180+
return listFilesWithLegacyFunctions(src, extensions);
65181
}
66182

67183
const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration';
@@ -163,6 +279,7 @@
163279

164280
const visitorKeyMap = new Map();
165281

282+
/** @type {Set<string>} */
166283
const ignoredFiles = new Set();
167284
const filesOutsideSrc = new Set();
168285

@@ -172,22 +289,30 @@
172289
* read all files matching the patterns in src and ignoreExports
173290
*
174291
* return all files matching src pattern, which are not matching the ignoreExports pattern
292+
* @type {(src: string, ignoreExports: string, context: import('eslint').Rule.RuleContext) => Set<string>}
175293
*/
176-
const resolveFiles = (src, ignoreExports, context) => {
294+
function resolveFiles(src, ignoreExports, context) {
177295
const extensions = Array.from(getFileExtensions(context.settings));
178296

179-
const srcFileList = listFilesToProcess(src, extensions);
297+
const srcFileList = listFilesToProcess(src, extensions, context);
180298

181299
// prepare list of ignored files
182-
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
183-
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));
300+
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions, context);
301+
302+
// The modern api will return a list of file paths, rather than an object
303+
if (ignoredFilesList.length && typeof ignoredFilesList[0] === 'string') {
304+
ignoredFilesList.forEach((filename) => ignoredFiles.add(filename));
305+
} else {
306+
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));
307+
}
184308

185309
// prepare list of source files, don't consider files from node_modules
310+
const resolvedFiles = srcFileList.length && typeof srcFileList[0] === 'string'
311+
? srcFileList.filter((filePath) => !isNodeModule(filePath))
312+
: flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename);
186313

187-
return new Set(
188-
flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename),
189-
);
190-
};
314+
return new Set(resolvedFiles);
315+
}
191316

192317
/**
193318
* parse all source files and build up 2 maps containing the existing imports and exports
@@ -226,7 +351,7 @@
226351
} else {
227352
exports.set(key, { whereUsed: new Set() });
228353
}
229-
const reexport = value.getImport();
354+
const reexport = value.getImport();
230355
if (!reexport) {
231356
return;
232357
}
@@ -329,6 +454,7 @@
329454
* prepare the lists of existing imports and exports - should only be executed once at
330455
* the start of a new eslint run
331456
*/
457+
/** @type {Set<string>} */
332458
let srcFiles;
333459
let lastPrepareKey;
334460
const doPreparation = (src, ignoreExports, context) => {

‎utils/ignore.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const log = require('debug')('eslint-plugin-import:utils:ignore');
1414
function makeValidExtensionSet(settings) {
1515
// start with explicit JS-parsed extensions
1616
/** @type {Set<import('./types').Extension>} */
17-
const exts = new Set(settings['import/extensions'] || ['.js']);
17+
const exts = new Set(settings['import/extensions'] || ['.js', '.mjs', '.cjs']);
1818

1919
// all alternate parser extensions are also valid
2020
if ('import/parsers' in settings) {
@@ -52,9 +52,13 @@ exports.hasValidExtension = hasValidExtension;
5252
/** @type {import('./ignore').default} */
5353
exports.default = function ignore(path, context) {
5454
// check extension whitelist first (cheap)
55-
if (!hasValidExtension(path, context)) { return true; }
55+
if (!hasValidExtension(path, context)) {
56+
return true;
57+
}
5658

57-
if (!('import/ignore' in context.settings)) { return false; }
59+
if (!('import/ignore' in context.settings)) {
60+
return false;
61+
}
5862
const ignoreStrings = context.settings['import/ignore'];
5963

6064
for (let i = 0; i < ignoreStrings.length; i++) {

0 commit comments

Comments
 (0)
Please sign in to comment.