fix(linter): check for flat config correctly in @nx/eslint:lint executor (#26350)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #22575
This commit is contained in:
Leosvel Pérez Espinosa 2024-06-05 12:36:48 +02:00 committed by GitHub
parent f8239debd0
commit f4b379f459
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 29 deletions

View File

@ -1,11 +1,11 @@
import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit'; import { joinPathFragments, type ExecutorContext } from '@nx/devkit';
import { ESLint } from 'eslint'; import type { ESLint } from 'eslint';
import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { mkdirSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path'; import { interpolate } from 'nx/src/tasks-runner/utils';
import { dirname, posix, resolve } from 'path';
import { findFlatConfigFile } from '../../utils/config-file';
import type { Schema } from './schema'; import type { Schema } from './schema';
import { resolveAndInstantiateESLint } from './utility/eslint-utils'; import { resolveAndInstantiateESLint } from './utility/eslint-utils';
import { interpolate } from 'nx/src/tasks-runner/utils';
export default async function run( export default async function run(
options: Schema, options: Schema,
@ -37,22 +37,18 @@ export default async function run(
const { printConfig, errorOnUnmatchedPattern, ...normalizedOptions } = const { printConfig, errorOnUnmatchedPattern, ...normalizedOptions } =
options; options;
/** // locate the flat config file if it exists starting from the project root
* Until ESLint v9 is released and the new so called flat config is the default const flatConfigFilePath = findFlatConfigFile(projectRoot, context.root);
* we only want to support it if the user has explicitly opted into it by converting const hasFlatConfig = flatConfigFilePath !== null;
* their root ESLint config to use eslint.config.js
*/
const hasFlatConfig = existsSync(
joinPathFragments(workspaceRoot, 'eslint.config.js')
);
// while standard eslint uses by default closest config to the file, if otherwise not specified, // while standard eslint uses by default closest config to the file, if otherwise not specified,
// the flat config would always use the root config, so we need to explicitly set it to the local one // the flat config would be resolved starting from the cwd, which we changed to the workspace root
// so we explicitly set the config path to the flat config file path we previously found
if (hasFlatConfig && !normalizedOptions.eslintConfig) { if (hasFlatConfig && !normalizedOptions.eslintConfig) {
const eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js'); normalizedOptions.eslintConfig = posix.relative(
if (existsSync(eslintConfigPath)) { systemRoot,
normalizedOptions.eslintConfig = eslintConfigPath; flatConfigFilePath
} );
} }
/** /**

View File

@ -1,4 +1,5 @@
import type { ESLint } from 'eslint'; import type { ESLint } from 'eslint';
import { isFlatConfig } from '../../../utils/config-file';
import { resolveESLintClass } from '../../../utils/resolve-eslint-class'; import { resolveESLintClass } from '../../../utils/resolve-eslint-class';
import type { Schema } from '../schema'; import type { Schema } from '../schema';
@ -7,11 +8,7 @@ export async function resolveAndInstantiateESLint(
options: Schema, options: Schema,
useFlatConfig = false useFlatConfig = false
) { ) {
if ( if (useFlatConfig && eslintConfigPath && !isFlatConfig(eslintConfigPath)) {
useFlatConfig &&
eslintConfigPath &&
!eslintConfigPath?.endsWith('eslint.config.js')
) {
throw new Error( throw new Error(
'When using the new Flat Config with ESLint, all configs must be named eslint.config.js and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files-new' 'When using the new Flat Config with ESLint, all configs must be named eslint.config.js and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files-new'
); );

View File

@ -1,19 +1,93 @@
import { joinPathFragments } from '@nx/devkit'; import { existsSync, statSync } from 'fs';
import { existsSync } from 'fs'; import { basename, dirname, join, resolve } from 'path';
export const ESLINT_CONFIG_FILENAMES = [ // TODO(leo): add support for eslint.config.mjs and eslint.config.cjs
export const ESLINT_FLAT_CONFIG_FILENAMES = ['eslint.config.js'];
export const ESLINT_OLD_CONFIG_FILENAMES = [
'.eslintrc', '.eslintrc',
'.eslintrc.js', '.eslintrc.js',
'.eslintrc.cjs', '.eslintrc.cjs',
'.eslintrc.yaml', '.eslintrc.yaml',
'.eslintrc.yml', '.eslintrc.yml',
'.eslintrc.json', '.eslintrc.json',
'eslint.config.js', ];
export const ESLINT_CONFIG_FILENAMES = [
...ESLINT_OLD_CONFIG_FILENAMES,
...ESLINT_FLAT_CONFIG_FILENAMES,
]; ];
export const baseEsLintConfigFile = '.eslintrc.base.json'; export const baseEsLintConfigFile = '.eslintrc.base.json';
export const baseEsLintFlatConfigFile = 'eslint.base.config.js'; export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
export function isFlatConfig(configFilePath: string): boolean { export function isFlatConfig(configFilePath: string): boolean {
return configFilePath.endsWith('.config.js'); const configFileName = basename(configFilePath);
return ESLINT_FLAT_CONFIG_FILENAMES.includes(configFileName);
}
// https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-resolution
export function findFlatConfigFile(
directory: string,
workspaceRoot: string
): string | null {
let currentDir = resolve(workspaceRoot, directory);
if (currentDir === workspaceRoot) {
return getConfigFileInDirectory(currentDir, ESLINT_FLAT_CONFIG_FILENAMES);
}
while (currentDir !== workspaceRoot) {
const configFilePath = getConfigFileInDirectory(
currentDir,
ESLINT_FLAT_CONFIG_FILENAMES
);
if (configFilePath) {
return configFilePath;
}
currentDir = dirname(currentDir);
}
return null;
}
export function findOldConfigFile(
filePathOrDirectory: string,
workspaceRoot: string
): string | null {
let currentDir = statSync(filePathOrDirectory).isDirectory()
? filePathOrDirectory
: dirname(filePathOrDirectory);
if (currentDir === workspaceRoot) {
return getConfigFileInDirectory(currentDir, ESLINT_OLD_CONFIG_FILENAMES);
}
while (currentDir !== workspaceRoot) {
const configFilePath = getConfigFileInDirectory(
currentDir,
ESLINT_OLD_CONFIG_FILENAMES
);
if (configFilePath) {
return configFilePath;
}
currentDir = dirname(currentDir);
}
return null;
}
function getConfigFileInDirectory(
directory: string,
candidateFileNames: string[]
): string | null {
for (const filename of candidateFileNames) {
const filePath = join(directory, filename);
if (existsSync(filePath)) {
return filePath;
}
}
return null;
} }