Skip to content

Commit f02ff85

Browse files
committed
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', }, }, ]; ``` Note: in order to fill a gap in a future API gap for the `no-unused-module`, this takes advantage of a *proposed* new API on the ESLint context, that currently only exists in a POC state (unreleased). Closes import-js#2556
1 parent dd81308 commit f02ff85

File tree

3 files changed

+97
-27
lines changed

3 files changed

+97
-27
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@
108108
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
109109
},
110110
"dependencies": {
111-
"@nodelib/fs.walk": "^2.0.0",
112111
"array-includes": "^3.1.7",
113112
"array.prototype.findlastindex": "^1.2.4",
114113
"array.prototype.flat": "^1.3.2",

src/core/fsWalk.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
10+
/**
11+
* Do a comprehensive walk of the provided src directory, and collect all entries. Filter out
12+
* any directories or entries using the optional filter functions.
13+
* @param {string} root - path to the root of the folder we're walking
14+
* @param {{ deepFilter?: ({name: string, path: string, dirent: Dirent}) => boolean, entryFilter?: ({name: string, path: string, dirent: Dirent}) => boolean }} options
15+
* @param {{name: string, path: string, dirent: Dirent}} currentEntry - entry for the current directory we're working in
16+
* @param {{name: string, path: string, dirent: Dirent}[]} existingEntries - list of all entries so far
17+
* @returns {{name: string, path: string, dirent: Dirent}[]} an array of directory entries
18+
*/
19+
export const walkSync = (root, options, currentEntry, existingEntries) => {
20+
const { readdirSync } = require('node:fs');
21+
22+
// Extract the filter functions. Default to evaluating true, if no filter passed in.
23+
const { deepFilter = (_) => true, entryFilter = (_) => true } = options;
24+
25+
let entryList = existingEntries || [];
26+
const currentRelativePath = currentEntry ? currentEntry.path : '.';
27+
const fullPath = currentEntry ? path.join(root, currentEntry.path) : root;
28+
29+
const dirents = readdirSync(fullPath, { withFileTypes: true });
30+
for (const dirent of dirents) {
31+
const entry = {
32+
name: dirent.name,
33+
path: path.join(currentRelativePath, dirent.name),
34+
dirent,
35+
};
36+
if (dirent.isDirectory() && deepFilter(entry)) {
37+
entryList.push(entry);
38+
entryList = walkSync(root, options, entry, entryList);
39+
} else if (dirent.isFile() && entryFilter(entry)) {
40+
entryList.push(entry);
41+
}
42+
}
43+
44+
return entryList;
45+
};

src/rules/no-unused-modules.js

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @author René Fermann
55
*/
66

7-
import * as fsWalk from '@nodelib/fs.walk';
7+
import * as fsWalk from '../core/fsWalk';
88
import { getFileExtensions } from 'eslint-module-utils/ignore';
99
import resolve from 'eslint-module-utils/resolve';
1010
import visit from 'eslint-module-utils/visit';
@@ -44,8 +44,8 @@ function listFilesWithModernApi(srcPaths, extensions, session) {
4444

4545
// Include the file if it's not marked as ignore by eslint and its extension is included in our list
4646
return (
47-
!session.isFileIgnored(fullEntryPath)
48-
&& extensions.find((extension) => entry.path.endsWith(extension))
47+
!session.isFileIgnored(fullEntryPath) &&
48+
extensions.find((extension) => entry.path.endsWith(extension))
4949
);
5050
},
5151
});
@@ -140,8 +140,10 @@ function listFilesWithLegacyFunctions(src, extensions) {
140140
listFilesToProcess: originalListFilesToProcess,
141141
} = require('eslint/lib/util/glob-util');
142142
const patterns = src.concat(
143-
flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`,
144-
),
143+
flatMap(src, (pattern) =>
144+
extensions.map((extension) =>
145+
/\*\*|\*\./.test(pattern) ? pattern : `${pattern}/**/*${extension}`,
146+
),
145147
),
146148
);
147149

@@ -162,9 +164,9 @@ function listFilesToProcess(src, extensions, context) {
162164
// Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support.
163165
// https://github.com/eslint/eslint/issues/18087
164166
if (
165-
context.session
166-
&& context.session.isFileIgnored
167-
&& context.session.isDirectoryIgnored
167+
context.session &&
168+
context.session.isFileIgnored &&
169+
context.session.isDirectoryIgnored
168170
) {
169171
return listFilesWithModernApi(src, extensions, context.session);
170172
} else {
@@ -175,7 +177,7 @@ function listFilesToProcess(src, extensions, context) {
175177
if (FileEnumerator) {
176178
return listFilesUsingFileEnumerator(FileEnumerator, src, extensions);
177179
} else {
178-
// If not, then we can try even older versions of this capability (listFilesToProcess)
180+
// If not, then we can try even older versions of this capability (listFilesToProcess)
179181
return listFilesWithLegacyFunctions(src, extensions);
180182
}
181183
}
@@ -283,7 +285,7 @@ const visitorKeyMap = new Map();
283285
const ignoredFiles = new Set();
284286
const filesOutsideSrc = new Set();
285287

286-
const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
288+
const isNodeModule = (path) => /\/(node_modules)\//.test(path);
287289

288290
/**
289291
* read all files matching the patterns in src and ignoreExports
@@ -317,7 +319,8 @@ const resolveFiles = (src, ignoreExports, context) => {
317319
);
318320
} else {
319321
resolvedFiles = new Set(
320-
flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename,
322+
flatMap(srcFileList, ({ filename }) =>
323+
isNodeModule(filename) ? [] : filename,
321324
),
322325
);
323326
}
@@ -487,9 +490,11 @@ const doPreparation = (src, ignoreExports, context) => {
487490
lastPrepareKey = prepareKey;
488491
};
489492

490-
const newNamespaceImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER);
493+
const newNamespaceImportExists = (specifiers) =>
494+
specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER);
491495

492-
const newDefaultImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER);
496+
const newDefaultImportExists = (specifiers) =>
497+
specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER);
493498

494499
const fileIsInPkg = (file) => {
495500
const { path, pkg } = readPkgUp({ cwd: file });
@@ -502,7 +507,8 @@ const fileIsInPkg = (file) => {
502507
};
503508

504509
const checkPkgFieldObject = (pkgField) => {
505-
const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value),
510+
const pkgFieldFiles = flatMap(values(pkgField), (value) =>
511+
typeof value === 'boolean' ? [] : join(basePath, value),
506512
);
507513

508514
if (includes(pkgFieldFiles, file)) {
@@ -681,14 +687,16 @@ module.exports = {
681687
exports = exportList.get(file);
682688

683689
if (!exports) {
684-
console.error(`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`);
690+
console.error(
691+
`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`,
692+
);
685693
}
686694

687695
// special case: export * from
688696
const exportAll = exports.get(EXPORT_ALL_DECLARATION);
689697
if (
690-
typeof exportAll !== 'undefined'
691-
&& exportedValue !== IMPORT_DEFAULT_SPECIFIER
698+
typeof exportAll !== 'undefined' &&
699+
exportedValue !== IMPORT_DEFAULT_SPECIFIER
692700
) {
693701
if (exportAll.whereUsed.size > 0) {
694702
return;
@@ -704,11 +712,13 @@ module.exports = {
704712
}
705713

706714
// exportsList will always map any imported value of 'default' to 'ImportDefaultSpecifier'
707-
const exportsKey = exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;
715+
const exportsKey =
716+
exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;
708717

709718
const exportStatement = exports.get(exportsKey);
710719

711-
const value = exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;
720+
const value =
721+
exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;
712722

713723
if (typeof exportStatement !== 'undefined') {
714724
if (exportStatement.whereUsed.size < 1) {
@@ -831,8 +841,8 @@ module.exports = {
831841
}
832842
value.forEach((val) => {
833843
if (
834-
val !== IMPORT_NAMESPACE_SPECIFIER
835-
&& val !== IMPORT_DEFAULT_SPECIFIER
844+
val !== IMPORT_NAMESPACE_SPECIFIER &&
845+
val !== IMPORT_DEFAULT_SPECIFIER
836846
) {
837847
oldImports.set(val, key);
838848
}
@@ -867,7 +877,10 @@ module.exports = {
867877
// support for export { value } from 'module'
868878
if (astNode.type === EXPORT_NAMED_DECLARATION) {
869879
if (astNode.source) {
870-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
880+
resolvedPath = resolve(
881+
astNode.source.raw.replace(/('|")/g, ''),
882+
context,
883+
);
871884
astNode.specifiers.forEach((specifier) => {
872885
const name = specifier.local.name || specifier.local.value;
873886
if (name === DEFAULT) {
@@ -880,12 +893,18 @@ module.exports = {
880893
}
881894

882895
if (astNode.type === EXPORT_ALL_DECLARATION) {
883-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
896+
resolvedPath = resolve(
897+
astNode.source.raw.replace(/('|")/g, ''),
898+
context,
899+
);
884900
newExportAll.add(resolvedPath);
885901
}
886902

887903
if (astNode.type === IMPORT_DECLARATION) {
888-
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
904+
resolvedPath = resolve(
905+
astNode.source.raw.replace(/('|")/g, ''),
906+
context,
907+
);
889908
if (!resolvedPath) {
890909
return;
891910
}
@@ -903,9 +922,16 @@ module.exports = {
903922
}
904923

905924
astNode.specifiers
906-
.filter((specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER && specifier.type !== IMPORT_NAMESPACE_SPECIFIER)
925+
.filter(
926+
(specifier) =>
927+
specifier.type !== IMPORT_DEFAULT_SPECIFIER &&
928+
specifier.type !== IMPORT_NAMESPACE_SPECIFIER,
929+
)
907930
.forEach((specifier) => {
908-
newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath);
931+
newImports.set(
932+
specifier.imported.name || specifier.imported.value,
933+
resolvedPath,
934+
);
909935
});
910936
}
911937
});

0 commit comments

Comments
 (0)