From db469585f3f531a597c67718149f6fb455ce18ab Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Thu, 16 Jan 2025 18:18:27 -0700 Subject: [PATCH] feat(core): improve base ESLint config filenames --- .../generators/convert-to-flat-config.json | 6 - .../convert-to-flat-config/generator.ts | 1 + .../convert-to-flat-config/schema.d.ts | 1 + .../convert-to-flat-config/schema.json | 6 - .../lint-project/lint-project.spec.ts | 289 ++++++++++++------ .../generators/lint-project/lint-project.ts | 7 +- .../src/generators/utils/eslint-file.spec.ts | 9 +- .../src/generators/utils/eslint-file.ts | 37 ++- packages/eslint/src/plugins/plugin.ts | 4 +- packages/eslint/src/utils/config-file.ts | 7 +- packages/eslint/src/utils/flat-config.ts | 8 +- 11 files changed, 249 insertions(+), 126 deletions(-) diff --git a/docs/generated/packages/eslint/generators/convert-to-flat-config.json b/docs/generated/packages/eslint/generators/convert-to-flat-config.json index 89c8d9d641ec2..03ad486e88255 100644 --- a/docs/generated/packages/eslint/generators/convert-to-flat-config.json +++ b/docs/generated/packages/eslint/generators/convert-to-flat-config.json @@ -13,12 +13,6 @@ "description": "Skip formatting files.", "default": false, "x-priority": "internal" - }, - "eslintConfigFormat": { - "type": "string", - "description": "The format of the ESLint configuration file", - "enum": ["cjs", "mjs"], - "default": "mjs" } }, "additionalProperties": false, diff --git a/packages/eslint/src/generators/convert-to-flat-config/generator.ts b/packages/eslint/src/generators/convert-to-flat-config/generator.ts index 5fc0e10c45d97..fcbf4b79beae2 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/generator.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/generator.ts @@ -39,6 +39,7 @@ export async function convertToFlatConfigGenerator( 'Only json and yaml eslint config files are supported for conversion' ); } + options.eslintConfigFormat ??= 'mjs'; const eslintIgnoreFiles = new Set(['.eslintignore']); diff --git a/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts b/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts index a61f5db080624..d73fb14827c60 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts @@ -1,4 +1,5 @@ export interface ConvertToFlatConfigGeneratorSchema { skipFormat?: boolean; + // Internal option eslintConfigFormat?: 'mjs' | 'cjs'; } diff --git a/packages/eslint/src/generators/convert-to-flat-config/schema.json b/packages/eslint/src/generators/convert-to-flat-config/schema.json index 22f38cd3b3307..f738885e50834 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/schema.json +++ b/packages/eslint/src/generators/convert-to-flat-config/schema.json @@ -10,12 +10,6 @@ "description": "Skip formatting files.", "default": false, "x-priority": "internal" - }, - "eslintConfigFormat": { - "type": "string", - "description": "The format of the ESLint configuration file", - "enum": ["cjs", "mjs"], - "default": "mjs" } }, "additionalProperties": false, diff --git a/packages/eslint/src/generators/lint-project/lint-project.spec.ts b/packages/eslint/src/generators/lint-project/lint-project.spec.ts index bd20bde641440..d8b84da2c5df8 100644 --- a/packages/eslint/src/generators/lint-project/lint-project.spec.ts +++ b/packages/eslint/src/generators/lint-project/lint-project.spec.ts @@ -42,119 +42,234 @@ describe('@nx/eslint:lint-project', () => { }); }); - it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => { - const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; - process.env.ESLINT_USE_FLAT_CONFIG = 'true'; - - // CJS config - tree.write('eslint.base.config.js', 'module.exports = {};'); - - await lintProjectGenerator(tree, { - ...defaultOptions, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - skipFormat: true, + describe('Eslint base config named eslint.base.config', () => { + it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + + // CJS config + tree.write('eslint.base.config.js', 'module.exports = {};'); + + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); + + expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.base.config.js"); + + module.exports = [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; }); - expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) - .toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.base.config.js"); + it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; - module.exports = [ - ...baseConfig - ]; - " - `); + // MJS config + tree.write('eslint.base.config.js', 'export default {};'); - process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; - }); + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); - it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => { - const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; - process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from "../../eslint.base.config.js"; - // MJS config - tree.write('eslint.base.config.js', 'export default {};'); + export default [ + ...baseConfig + ]; + " + `); - await lintProjectGenerator(tree, { - ...defaultOptions, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - skipFormat: true, + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; }); - expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) - .toMatchInlineSnapshot(` - "import baseConfig from "../../eslint.base.config.js"; + it('should generate a flat eslint config format based on base config (mjs)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + + // MJS config + tree.write('eslint.base.config.mjs', 'export default {};'); + + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + eslintConfigFormat: 'mjs', + }); + + expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from "../../eslint.base.config.mjs"; + + export default [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); - export default [ - ...baseConfig - ]; - " - `); + it('should generate a flat eslint config format based on base config CJS', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; - process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; - }); + // CJS config + tree.write('eslint.base.config.cjs', 'module.exports = {};'); - it('should generate a flat eslint config format based on base config (mjs)', async () => { - const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; - process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); - // MJS config - tree.write('eslint.base.config.mjs', 'export default {};'); + expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.base.config.cjs"); - await lintProjectGenerator(tree, { - ...defaultOptions, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - skipFormat: true, - eslintConfigFormat: 'mjs', + module.exports = [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; }); + }); - expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) - .toMatchInlineSnapshot(` - "import baseConfig from "../../eslint.base.config.mjs"; + describe('Eslint base config named eslint.config', () => { + it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + + // CJS config + tree.write('eslint.config.js', 'module.exports = {};'); + + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); + + expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.js"); + + module.exports = [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); - export default [ - ...baseConfig - ]; - " - `); + it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; - process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; - }); + // MJS config + tree.write('eslint.config.js', 'export default {};'); - it('should generate a flat eslint config format based on base config CJS', async () => { - const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; - process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); - // CJS config - tree.write('eslint.config.cjs', 'module.exports = {};'); + expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from "../../eslint.config.js"; - await lintProjectGenerator(tree, { - ...defaultOptions, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - skipFormat: true, + export default [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; }); - console.log(tree.children('.')); + it('should generate a flat eslint config format based on base config (mjs)', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + + // MJS config + tree.write('eslint.config.mjs', 'export default {};'); + + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + eslintConfigFormat: 'mjs', + }); + + expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from "../../eslint.config.mjs"; + + export default [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); - expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) - .toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); + it('should generate a flat eslint config format based on base config CJS', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; - module.exports = [ - ...baseConfig - ]; - " - `); + // CJS config + tree.write('eslint.config.cjs', 'module.exports = {};'); - process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + }); + + expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require("../../eslint.config.cjs"); + + module.exports = [ + ...baseConfig + ]; + " + `); + + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); }); it('should generate a flat eslint base config ESM', async () => { diff --git a/packages/eslint/src/generators/lint-project/lint-project.ts b/packages/eslint/src/generators/lint-project/lint-project.ts index 76fc2a1bb8c81..1128837a6c44c 100644 --- a/packages/eslint/src/generators/lint-project/lint-project.ts +++ b/packages/eslint/src/generators/lint-project/lint-project.ts @@ -35,7 +35,7 @@ import { } from '../utils/flat-config/ast-utils'; import { baseEsLintConfigFile, - baseEsLintFlatConfigFile, + BASE_ESLINT_CONFIG_FILENAMES, } from '../../utils/config-file'; import { hasEslintPlugin } from '../utils/plugin'; import { setupRootEsLint } from './setup-root-eslint'; @@ -344,8 +344,9 @@ function isBuildableLibraryProject( function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean { // the base config is already created, migration has been done if ( - tree.exists(baseEsLintConfigFile) || - tree.exists(baseEsLintFlatConfigFile) + [baseEsLintConfigFile, ...BASE_ESLINT_CONFIG_FILENAMES].some((f) => + tree.exists(f) + ) ) { return false; } diff --git a/packages/eslint/src/generators/utils/eslint-file.spec.ts b/packages/eslint/src/generators/utils/eslint-file.spec.ts index dd5976b08202f..f0428d1bab30b 100644 --- a/packages/eslint/src/generators/utils/eslint-file.spec.ts +++ b/packages/eslint/src/generators/utils/eslint-file.spec.ts @@ -2,8 +2,8 @@ import { readJson, type Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import * as devkitInternals from 'nx/src/devkit-internals'; import { + BASE_ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES, - baseEsLintConfigFile, } from '../../utils/config-file'; import { addExtendsToLintConfig, @@ -32,12 +32,11 @@ describe('@nx/eslint:lint-file', () => { } ); - test.each(ESLINT_CONFIG_FILENAMES)( - 'should return base file instead %p when calling findEslintFile', + test.each(BASE_ESLINT_CONFIG_FILENAMES)( + 'should return base file %p when calling findEslintFile', (eslintFileName) => { - tree.write(baseEsLintConfigFile, '{}'); tree.write(eslintFileName, '{}'); - expect(findEslintFile(tree)).toBe(baseEsLintConfigFile); + expect(findEslintFile(tree)).toBe(eslintFileName); } ); }); diff --git a/packages/eslint/src/generators/utils/eslint-file.ts b/packages/eslint/src/generators/utils/eslint-file.ts index 19604b0e19d04..53bcff7095bc0 100644 --- a/packages/eslint/src/generators/utils/eslint-file.ts +++ b/packages/eslint/src/generators/utils/eslint-file.ts @@ -12,9 +12,8 @@ import type { Linter } from 'eslint'; import { gte } from 'semver'; import { baseEsLintConfigFile, - baseEsLintFlatConfigFile, ESLINT_CONFIG_FILENAMES, - legacyBaseEsLintFlatConfigFile, + BASE_ESLINT_CONFIG_FILENAMES, } from '../../utils/config-file'; import { eslintFlatConfigFilenames, @@ -45,17 +44,15 @@ export function findEslintFile( tree: Tree, projectRoot?: string ): string | null { - if (projectRoot === undefined && tree.exists(baseEsLintConfigFile)) { - return baseEsLintConfigFile; - } - if (projectRoot === undefined && tree.exists(baseEsLintFlatConfigFile)) { - return baseEsLintFlatConfigFile; - } - if ( - projectRoot === undefined && - tree.exists(legacyBaseEsLintFlatConfigFile) - ) { - return legacyBaseEsLintFlatConfigFile; + if (projectRoot === undefined) { + for (const file of [ + baseEsLintConfigFile, + ...BASE_ESLINT_CONFIG_FILENAMES, + ]) { + if (tree.exists(file)) { + return file; + } + } } projectRoot ??= ''; for (const file of ESLINT_CONFIG_FILENAMES) { @@ -227,7 +224,12 @@ export function addOverrideToLintConfig( if (useFlatConfig(tree)) { let fileName: string; if (isBase) { - fileName = joinPathFragments(root, baseEsLintFlatConfigFile); + for (const file of BASE_ESLINT_CONFIG_FILENAMES) { + if (tree.exists(joinPathFragments(root, file))) { + fileName = joinPathFragments(root, file); + break; + } + } } else { for (const f of eslintFlatConfigFilenames) { if (tree.exists(joinPathFragments(root, f))) { @@ -338,7 +340,12 @@ export function lintConfigHasOverride( checkBaseConfig && findEslintFile(tree, root).includes('.base'); if (isBase) { - fileName = joinPathFragments(root, baseEsLintFlatConfigFile); + for (const file of BASE_ESLINT_CONFIG_FILENAMES) { + if (tree.exists(joinPathFragments(root, file))) { + fileName = joinPathFragments(root, file); + break; + } + } } if (useFlatConfig(tree)) { if (!fileName) { diff --git a/packages/eslint/src/plugins/plugin.ts b/packages/eslint/src/plugins/plugin.ts index 1ece1059d07bd..5bcb090b8259e 100644 --- a/packages/eslint/src/plugins/plugin.ts +++ b/packages/eslint/src/plugins/plugin.ts @@ -21,7 +21,7 @@ import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { gte } from 'semver'; import { baseEsLintConfigFile, - baseEsLintFlatConfigFile, + BASE_ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES, isFlatConfig, } from '../utils/config-file'; @@ -405,7 +405,7 @@ function getProjectUsingESLintConfig( ): CreateNodesResult['projects'][string] | null { const rootEslintConfig = [ baseEsLintConfigFile, - baseEsLintFlatConfigFile, + ...BASE_ESLINT_CONFIG_FILENAMES, ...ESLINT_CONFIG_FILENAMES, ].find((f) => existsSync(join(context.workspaceRoot, f))); diff --git a/packages/eslint/src/utils/config-file.ts b/packages/eslint/src/utils/config-file.ts index a395c0e832316..130b94cff2fdd 100644 --- a/packages/eslint/src/utils/config-file.ts +++ b/packages/eslint/src/utils/config-file.ts @@ -1,6 +1,9 @@ import { existsSync, statSync } from 'fs'; import { basename, dirname, join, resolve } from 'path'; -import { eslintFlatConfigFilenames } from './flat-config'; +import { + baseEslintConfigFilenames, + eslintFlatConfigFilenames, +} from './flat-config'; export const ESLINT_FLAT_CONFIG_FILENAMES = eslintFlatConfigFilenames; @@ -18,6 +21,8 @@ export const ESLINT_CONFIG_FILENAMES = [ ...ESLINT_FLAT_CONFIG_FILENAMES, ]; +export const BASE_ESLINT_CONFIG_FILENAMES = baseEslintConfigFilenames; + export const baseEsLintConfigFile = '.eslintrc.base.json'; export const baseEsLintFlatConfigFile = 'eslint.base.config.mjs'; // Make sure we can handle previous file extension as well for migrations or custom generators. diff --git a/packages/eslint/src/utils/flat-config.ts b/packages/eslint/src/utils/flat-config.ts index 42b40e66053a0..61ab113ab2222 100644 --- a/packages/eslint/src/utils/flat-config.ts +++ b/packages/eslint/src/utils/flat-config.ts @@ -1,13 +1,19 @@ import { Tree } from '@nx/devkit'; import { gte } from 'semver'; -// todo: add support for eslint.config.mjs, export const eslintFlatConfigFilenames = [ 'eslint.config.cjs', 'eslint.config.js', 'eslint.config.mjs', ]; +export const baseEslintConfigFilenames = [ + 'eslint.base.js', + 'eslint.base.config.cjs', + 'eslint.base.config.js', + 'eslint.base.config.mjs', +]; + export function getRootESLintFlatConfigFilename(tree: Tree): string { for (const file of eslintFlatConfigFilenames) { if (tree.exists(file)) {