Skip to content

Commit

Permalink
feat: add support for Flat Config
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.

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',
    },
  },
];
```
  • Loading branch information
michaelfaith committed Aug 18, 2024
1 parent 98d1091 commit dd81308
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 55 deletions.
2 changes: 1 addition & 1 deletion config/flat/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
module.exports = {
settings: {
'import/extensions': ['.js', '.jsx'],
'import/extensions': ['.js', '.jsx', '.mjs', '.cjs'],
},
languageOptions: {
parserOptions: {
Expand Down
2 changes: 0 additions & 2 deletions config/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
* if you don't enable these settings at the top level.
*/
module.exports = {

settings: {
'import/extensions': ['.js', '.jsx'],
},

parserOptions: {
ecmaFeatures: { jsx: true },
},

};
2 changes: 1 addition & 1 deletion config/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// `.ts`/`.tsx`/`.js`/`.jsx` implementation.
const typeScriptExtensions = ['.ts', '.cts', '.mts', '.tsx'];

const allExtensions = [...typeScriptExtensions, '.js', '.jsx'];
const allExtensions = [...typeScriptExtensions, '.js', '.jsx', '.mjs', '.cjs'];

module.exports = {
settings: {
Expand Down
2 changes: 1 addition & 1 deletion examples/flat/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default [
ecmaVersion: 'latest',
sourceType: 'module',
},
ignores: ['eslint.config.js', '**/exports-unused.ts'],
ignores: ['eslint.config.mjs', '**/exports-unused.ts'],
rules: {
'no-unused-vars': 'off',
'import/no-dynamic-require': 'warn',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
},
"dependencies": {
"@nodelib/fs.walk": "^2.0.0",
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.4",
"array.prototype.flat": "^1.3.2",
Expand Down
238 changes: 191 additions & 47 deletions src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* @author René Fermann
*/

import * as fsWalk from '@nodelib/fs.walk';
import { getFileExtensions } from 'eslint-module-utils/ignore';
import resolve from 'eslint-module-utils/resolve';
import visit from 'eslint-module-utils/visit';
import { dirname, join } from 'path';
import { dirname, join, resolve as resolvePath } from 'path';
import readPkgUp from 'eslint-module-utils/readPkgUp';
import values from 'object.values';
import includes from 'array-includes';
Expand All @@ -17,51 +18,167 @@ import ExportMapBuilder from '../exportMap/builder';
import recursivePatternCapture from '../exportMap/patternCapture';
import docsUrl from '../docsUrl';

let FileEnumerator;
let listFilesToProcess;
/**
* Given a source root and list of supported extensions, use fsWalk and the
* new `eslint` `context.session` api to build the list of files we want to operate on
* @param {string[]} srcPaths array of source paths (for flat config this should just be a singular root (e.g. cwd))
* @param {string[]} extensions list of supported extensions
* @param session eslint context session object
* @returns list of files to operate on
*/
function listFilesWithModernApi(srcPaths, extensions, session) {
const files = [];

for (let i = 0; i < srcPaths.length; i++) {
const src = srcPaths[i];
// Use walkSync along with the new session api to gather the list of files
const entries = fsWalk.walkSync(src, {
deepFilter(entry) {
const fullEntryPath = resolvePath(src, entry.path);

// Include the directory if it's not marked as ignore by eslint
return !session.isDirectoryIgnored(fullEntryPath);
},
entryFilter(entry) {
const fullEntryPath = resolvePath(src, entry.path);

// Include the file if it's not marked as ignore by eslint and its extension is included in our list
return (
!session.isFileIgnored(fullEntryPath)
&& extensions.find((extension) => entry.path.endsWith(extension))
);
},
});

// Filter out directories and map entries to their paths
files.push(
...entries
.filter((entry) => !entry.dirent.isDirectory())
.map((entry) => entry.path),
);
}
return files;
}

/**
* Attempt to load the internal `FileEnumerator` class, which has existed in a couple
* of different places, depending on the version of `eslint`. Try requiring it from both
* locations.
* @returns Returns the `FileEnumerator` class if its requirable, otherwise `undefined`.
*/
function requireFileEnumerator() {
let FileEnumerator;

try {
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
} catch (e) {
// Try getting it from the eslint private / deprecated api
try {
// has been moved to eslint/lib/cli-engine/file-enumerator in version 6
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
({ FileEnumerator } = require('eslint/use-at-your-own-risk'));
} catch (e) {
// Absorb this if it's MODULE_NOT_FOUND
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}

// If not there, then try getting it from eslint/lib/cli-engine/file-enumerator (moved there in v6)
try {
// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-utils');

// Prevent passing invalid options (extensions array) to old versions of the function.
// https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280
// https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269
listFilesToProcess = function (src, extensions) {
return originalListFilesToProcess(src, {
extensions,
});
};
({ FileEnumerator } = require('eslint/lib/cli-engine/file-enumerator'));
} catch (e) {
const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util');

listFilesToProcess = function (src, extensions) {
const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`)));

return originalListFilesToProcess(patterns);
};
// Absorb this if it's MODULE_NOT_FOUND
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
}
return FileEnumerator;
}

if (FileEnumerator) {
listFilesToProcess = function (src, extensions) {
const e = new FileEnumerator({
extensions,
});
/**
*
* @param FileEnumerator the `FileEnumerator` class from `eslint`'s internal api
* @param {string} src path to the src root
* @param {string[]} extensions list of supported extensions
* @returns list of files to operate on
*/
function listFilesUsingFileEnumerator(FileEnumerator, src, extensions) {
const e = new FileEnumerator({
extensions,
});

return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({
const listOfFiles = Array.from(
e.iterateFiles(src),
({ filePath, ignored }) => ({
ignored,
filename: filePath,
}));
};
}),
);
return listOfFiles;
}

/**
* Attempt to require old versions of the file enumeration capability from v6 `eslint` and earlier, and use
* those functions to provide the list of files to operate on
* @param {string} src path to the src root
* @param {string[]} extensions list of supported extensions
* @returns list of files to operate on
*/
function listFilesWithLegacyFunctions(src, extensions) {
try {
// From v5.3 - v6
const {
listFilesToProcess: originalListFilesToProcess,
} = require('eslint/lib/util/glob-utils');
return originalListFilesToProcess(src, {
extensions,
});
} catch (e) {
// Absorb this if it's MODULE_NOT_FOUND
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}

// Last place to try (pre v5.3)
const {
listFilesToProcess: originalListFilesToProcess,
} = require('eslint/lib/util/glob-util');
const patterns = src.concat(
flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`,
),
),
);

return originalListFilesToProcess(patterns);
}
}

/**
* Given a src pattern and list of supported extensions, return a list of files to process
* with this rule.
* @param {string} src - file, directory, or glob pattern of files to act on
* @param {string[]} extensions - list of supported file extensions
* @param {object} context - the eslint context object
* @returns the list of files that this rule will evaluate.
*/
function listFilesToProcess(src, extensions, context) {
// If the context object has the new session functions, then prefer those
// Otherwise, fallback to using the deprecated `FileEnumerator` for legacy support.
// https://github.com/eslint/eslint/issues/18087
if (
context.session
&& context.session.isFileIgnored
&& context.session.isDirectoryIgnored
) {
return listFilesWithModernApi(src, extensions, context.session);
} else {
// Fallback to og FileEnumerator
const FileEnumerator = requireFileEnumerator();

// If we got the FileEnumerator, then let's go with that
if (FileEnumerator) {
return listFilesUsingFileEnumerator(FileEnumerator, src, extensions);
} else {
// If not, then we can try even older versions of this capability (listFilesToProcess)
return listFilesWithLegacyFunctions(src, extensions);
}
}
}

const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration';
Expand Down Expand Up @@ -176,17 +293,35 @@ const isNodeModule = (path) => (/\/(node_modules)\//).test(path);
const resolveFiles = (src, ignoreExports, context) => {
const extensions = Array.from(getFileExtensions(context.settings));

const srcFileList = listFilesToProcess(src, extensions);
const srcFileList = listFilesToProcess(src, extensions, context);

// prepare list of ignored files
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));
const ignoredFilesList = listFilesToProcess(
ignoreExports,
extensions,
context,
);

// prepare list of source files, don't consider files from node_modules
// The modern api will return a list of file paths, rather than an object
if (ignoredFilesList.length && typeof ignoredFilesList[0] === 'string') {
ignoredFiles.push(...ignoredFilesList);
} else {
ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename));
}

return new Set(
flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename),
);
// prepare list of source files, don't consider files from node_modules
let resolvedFiles;
if (srcFileList.length && typeof srcFileList[0] === 'string') {
resolvedFiles = new Set(
srcFileList.filter((filePath) => !isNodeModule(filePath)),
);
} else {
resolvedFiles = new Set(
flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename,
),
);
}
return resolvedFiles;
};

/**
Expand Down Expand Up @@ -226,7 +361,7 @@ const prepareImportsAndExports = (srcFiles, context) => {
} else {
exports.set(key, { whereUsed: new Set() });
}
const reexport = value.getImport();
const reexport = value.getImport();
if (!reexport) {
return;
}
Expand Down Expand Up @@ -367,7 +502,8 @@ const fileIsInPkg = (file) => {
};

const checkPkgFieldObject = (pkgField) => {
const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value));
const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value),
);

if (includes(pkgFieldFiles, file)) {
return true;
Expand Down Expand Up @@ -414,7 +550,8 @@ module.exports = {
type: 'suggestion',
docs: {
category: 'Helpful warnings',
description: 'Forbid modules without exports, or exports without matching import in another module.',
description:
'Forbid modules without exports, or exports without matching import in another module.',
url: docsUrl('no-unused-modules'),
},
schema: [{
Expand Down Expand Up @@ -483,7 +620,9 @@ module.exports = {
doPreparation(src, ignoreExports, context);
}

const file = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
const file = context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename();

const checkExportPresence = (node) => {
if (!missingExports) {
Expand Down Expand Up @@ -547,7 +686,10 @@ module.exports = {

// special case: export * from
const exportAll = exports.get(EXPORT_ALL_DECLARATION);
if (typeof exportAll !== 'undefined' && exportedValue !== IMPORT_DEFAULT_SPECIFIER) {
if (
typeof exportAll !== 'undefined'
&& exportedValue !== IMPORT_DEFAULT_SPECIFIER
) {
if (exportAll.whereUsed.size > 0) {
return;
}
Expand Down Expand Up @@ -612,7 +754,9 @@ module.exports = {
if (specifiers.length > 0) {
specifiers.forEach((specifier) => {
if (specifier.exported) {
newExportIdentifiers.add(specifier.exported.name || specifier.exported.value);
newExportIdentifiers.add(
specifier.exported.name || specifier.exported.value,
);
}
});
}
Expand Down
10 changes: 7 additions & 3 deletions utils/ignore.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const log = require('debug')('eslint-plugin-import:utils:ignore');
function makeValidExtensionSet(settings) {
// start with explicit JS-parsed extensions
/** @type {Set<import('./types').Extension>} */
const exts = new Set(settings['import/extensions'] || ['.js']);
const exts = new Set(settings['import/extensions'] || ['.js', '.mjs', '.cjs']);

// all alternate parser extensions are also valid
if ('import/parsers' in settings) {
Expand Down Expand Up @@ -52,9 +52,13 @@ exports.hasValidExtension = hasValidExtension;
/** @type {import('./ignore').default} */
exports.default = function ignore(path, context) {
// check extension whitelist first (cheap)
if (!hasValidExtension(path, context)) { return true; }
if (!hasValidExtension(path, context)) {
return true;
}

if (!('import/ignore' in context.settings)) { return false; }
if (!('import/ignore' in context.settings)) {
return false;
}
const ignoreStrings = context.settings['import/ignore'];

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

0 comments on commit dd81308

Please sign in to comment.