Skip to content

Commit

Permalink
fix(linter): check for flat config correctly in @nx/eslint:lint execu…
Browse files Browse the repository at this point in the history
…tor (#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
  • Loading branch information
leosvelperez authored Jun 5, 2024
1 parent f8239de commit f4b379f
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 29 deletions.
34 changes: 15 additions & 19 deletions packages/eslint/src/executors/lint/lint.impl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit';
import { ESLint } from 'eslint';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path';

import { joinPathFragments, type ExecutorContext } from '@nx/devkit';
import type { ESLint } from 'eslint';
import { mkdirSync, writeFileSync } from 'fs';
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 { resolveAndInstantiateESLint } from './utility/eslint-utils';
import { interpolate } from 'nx/src/tasks-runner/utils';

export default async function run(
options: Schema,
Expand Down Expand Up @@ -37,22 +37,18 @@ export default async function run(
const { printConfig, errorOnUnmatchedPattern, ...normalizedOptions } =
options;

/**
* Until ESLint v9 is released and the new so called flat config is the default
* we only want to support it if the user has explicitly opted into it by converting
* their root ESLint config to use eslint.config.js
*/
const hasFlatConfig = existsSync(
joinPathFragments(workspaceRoot, 'eslint.config.js')
);
// locate the flat config file if it exists starting from the project root
const flatConfigFilePath = findFlatConfigFile(projectRoot, context.root);
const hasFlatConfig = flatConfigFilePath !== null;

// 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) {
const eslintConfigPath = joinPathFragments(projectRoot, 'eslint.config.js');
if (existsSync(eslintConfigPath)) {
normalizedOptions.eslintConfig = eslintConfigPath;
}
normalizedOptions.eslintConfig = posix.relative(
systemRoot,
flatConfigFilePath
);
}

/**
Expand Down
7 changes: 2 additions & 5 deletions packages/eslint/src/executors/lint/utility/eslint-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ESLint } from 'eslint';
import { isFlatConfig } from '../../../utils/config-file';
import { resolveESLintClass } from '../../../utils/resolve-eslint-class';
import type { Schema } from '../schema';

Expand All @@ -7,11 +8,7 @@ export async function resolveAndInstantiateESLint(
options: Schema,
useFlatConfig = false
) {
if (
useFlatConfig &&
eslintConfigPath &&
!eslintConfigPath?.endsWith('eslint.config.js')
) {
if (useFlatConfig && eslintConfigPath && !isFlatConfig(eslintConfigPath)) {
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'
);
Expand Down
84 changes: 79 additions & 5 deletions packages/eslint/src/utils/config-file.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,93 @@
import { joinPathFragments } from '@nx/devkit';
import { existsSync } from 'fs';
import { existsSync, statSync } 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.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.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 baseEsLintFlatConfigFile = 'eslint.base.config.js';

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;
}

0 comments on commit f4b379f

Please sign in to comment.