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',
    },
  },
];
```

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
  • Loading branch information
michaelfaith committed Aug 18, 2024
1 parent dd81308 commit e4ae179
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 11 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@
"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
45 changes: 45 additions & 0 deletions src/core/fsWalk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* This is intended to provide similar capability as the sync api from @nodelib/fs.walk, until `eslint-plugin-import`
* is willing to modernize and update their minimum node version to at least v16. I intentionally made the
* shape of the API (for the part we're using) the same as @nodelib/fs.walk so that that can be swapped in
* when the repo is ready for it.
*/

import path from 'path';

/**
* Do a comprehensive walk of the provided src directory, and collect all entries. Filter out
* any directories or entries using the optional filter functions.
* @param {string} root - path to the root of the folder we're walking
* @param {{ deepFilter?: ({name: string, path: string, dirent: Dirent}) => boolean, entryFilter?: ({name: string, path: string, dirent: Dirent}) => boolean }} options
* @param {{name: string, path: string, dirent: Dirent}} currentEntry - entry for the current directory we're working in
* @param {{name: string, path: string, dirent: Dirent}[]} existingEntries - list of all entries so far
* @returns {{name: string, path: string, dirent: Dirent}[]} an array of directory entries
*/
export const walkSync = (root, options, currentEntry, existingEntries) => {
const { readdirSync } = require('node:fs');

// Extract the filter functions. Default to evaluating true, if no filter passed in.
const { deepFilter = () => true, entryFilter = () => true } = options;

let entryList = existingEntries || [];
const currentRelativePath = currentEntry ? currentEntry.path : '.';
const fullPath = currentEntry ? path.join(root, currentEntry.path) : root;

const dirents = readdirSync(fullPath, { withFileTypes: true });
for (const dirent of dirents) {
const entry = {
name: dirent.name,
path: path.join(currentRelativePath, dirent.name),
dirent,
};
if (dirent.isDirectory() && deepFilter(entry)) {
entryList.push(entry);
entryList = walkSync(root, options, entry, entryList);
} else if (dirent.isFile() && entryFilter(entry)) {
entryList.push(entry);
}
}

return entryList;
};
37 changes: 27 additions & 10 deletions src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @author René Fermann
*/

import * as fsWalk from '@nodelib/fs.walk';
import * as fsWalk from '../core/fsWalk';
import { getFileExtensions } from 'eslint-module-utils/ignore';
import resolve from 'eslint-module-utils/resolve';
import visit from 'eslint-module-utils/visit';
Expand Down Expand Up @@ -175,7 +175,7 @@ function listFilesToProcess(src, extensions, context) {
if (FileEnumerator) {
return listFilesUsingFileEnumerator(FileEnumerator, src, extensions);
} else {
// If not, then we can try even older versions of this capability (listFilesToProcess)
// If not, then we can try even older versions of this capability (listFilesToProcess)
return listFilesWithLegacyFunctions(src, extensions);
}
}
Expand Down Expand Up @@ -681,7 +681,9 @@ module.exports = {
exports = exportList.get(file);

if (!exports) {
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!`);
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!`,
);
}

// special case: export * from
Expand All @@ -704,11 +706,11 @@ module.exports = {
}

// exportsList will always map any imported value of 'default' to 'ImportDefaultSpecifier'
const exportsKey = exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;
const exportsKey = exportedValue === DEFAULT ? IMPORT_DEFAULT_SPECIFIER : exportedValue;

const exportStatement = exports.get(exportsKey);

const value = exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;
const value = exportsKey === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportsKey;

if (typeof exportStatement !== 'undefined') {
if (exportStatement.whereUsed.size < 1) {
Expand Down Expand Up @@ -867,7 +869,10 @@ module.exports = {
// support for export { value } from 'module'
if (astNode.type === EXPORT_NAMED_DECLARATION) {
if (astNode.source) {
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
resolvedPath = resolve(
astNode.source.raw.replace(/('|")/g, ''),
context,
);
astNode.specifiers.forEach((specifier) => {
const name = specifier.local.name || specifier.local.value;
if (name === DEFAULT) {
Expand All @@ -880,12 +885,18 @@ module.exports = {
}

if (astNode.type === EXPORT_ALL_DECLARATION) {
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
resolvedPath = resolve(
astNode.source.raw.replace(/('|")/g, ''),
context,
);
newExportAll.add(resolvedPath);
}

if (astNode.type === IMPORT_DECLARATION) {
resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context);
resolvedPath = resolve(
astNode.source.raw.replace(/('|")/g, ''),
context,
);
if (!resolvedPath) {
return;
}
Expand All @@ -903,9 +914,15 @@ module.exports = {
}

astNode.specifiers
.filter((specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER && specifier.type !== IMPORT_NAMESPACE_SPECIFIER)
.filter(
(specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER
&& specifier.type !== IMPORT_NAMESPACE_SPECIFIER,
)
.forEach((specifier) => {
newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath);
newImports.set(
specifier.imported.name || specifier.imported.value,
resolvedPath,
);
});
}
});
Expand Down

0 comments on commit e4ae179

Please sign in to comment.