diff --git a/e2e/eslint/src/linter.test.ts b/e2e/eslint/src/linter.test.ts index 401e593094..ab96bf8338 100644 --- a/e2e/eslint/src/linter.test.ts +++ b/e2e/eslint/src/linter.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { + checkFilesDoNotExist, checkFilesExist, cleanupProject, createFile, @@ -521,6 +522,43 @@ describe('Linter', () => { ); }); }); + + describe('flat config', () => { + beforeAll(() => { + runCLI(`generate @nx/eslint:convert-to-flat-config`); + }); + + it('should generate new projects using flat config', () => { + const reactLib = uniq('react-lib'); + const jsLib = uniq('js-lib'); + + runCLI( + `generate @nx/react:lib ${reactLib} --directory=${reactLib} --projectNameAndRootFormat=as-provided` + ); + runCLI( + `generate @nx/js:lib ${jsLib} --directory=${jsLib} --projectNameAndRootFormat=as-provided` + ); + + checkFilesExist( + `${reactLib}/eslint.config.js`, + `${jsLib}/eslint.config.js` + ); + checkFilesDoNotExist( + `${reactLib}/.eslintrc.json`, + `${jsLib}/.eslintrc.json` + ); + + // validate that the new projects are linted successfully + let output = runCLI(`lint ${reactLib}`); + expect(output).toContain( + `Successfully ran target lint for project ${reactLib}` + ); + output = runCLI(`lint ${jsLib}`); + expect(output).toContain( + `Successfully ran target lint for project ${jsLib}` + ); + }); + }); }); describe('Root projects migration', () => { diff --git a/packages/eslint/src/generators/utils/eslint-file.spec.ts b/packages/eslint/src/generators/utils/eslint-file.spec.ts index 8d22fb633a..54f48749c2 100644 --- a/packages/eslint/src/generators/utils/eslint-file.spec.ts +++ b/packages/eslint/src/generators/utils/eslint-file.spec.ts @@ -2,6 +2,7 @@ import { addExtendsToLintConfig, findEslintFile, lintConfigHasOverride, + replaceOverridesInLintConfig, } from './eslint-file'; import { Tree, readJson } from '@nx/devkit'; @@ -120,4 +121,124 @@ describe('@nx/eslint:lint-file', () => { ]); }); }); + + describe('replaceOverridesInLintConfig', () => { + it('should replace overrides when using flat config', () => { + tree.write('eslint.config.js', 'module.exports = {};'); + tree.write( + 'apps/demo/eslint.config.js', + `const baseConfig = require("../../eslint.config.js"); + +module.exports = [ + ...baseConfig, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: {} + }, + { + files: [ + "**/*.ts", + "**/*.tsx" + ], + rules: {} + }, + { + files: [ + "**/*.js", + "**/*.jsx" + ], + rules: {} + } +];` + ); + + replaceOverridesInLintConfig(tree, 'apps/demo', [ + { + files: ['*.ts'], + extends: [ + 'plugin:@nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'myOrg', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'my-org', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['*.html'], + extends: ['plugin:@nx/angular-template'], + rules: {}, + }, + ]); + + expect(tree.read('apps/demo/eslint.config.js', 'utf-8')) + .toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const baseConfig = require("../../eslint.config.js"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + + module.exports = [ + ...baseConfig, + ...compat.config({ extends: [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] }).map(config => ({ + ...config, + files: ["**/*.ts"], + rules: { + ...config.rules, + "@angular-eslint/directive-selector": [ + "error", + { + type: "attribute", + prefix: "myOrg", + style: "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + type: "element", + prefix: "my-org", + style: "kebab-case" + } + ] + } + })), + ...compat.config({ extends: ["plugin:@nx/angular-template"] }).map(config => ({ + ...config, + files: ["**/*.html"], + rules: { + ...config.rules + } + })), + ];" + `); + }); + }); }); diff --git a/packages/eslint/src/generators/utils/eslint-file.ts b/packages/eslint/src/generators/utils/eslint-file.ts index d4a9f0a0a5..90e23965f4 100644 --- a/packages/eslint/src/generators/utils/eslint-file.ts +++ b/packages/eslint/src/generators/utils/eslint-file.ts @@ -291,7 +291,7 @@ export function replaceOverridesInLintConfig( content = removeOverridesFromLintConfig(content); overrides.forEach((override) => { const flatOverride = generateFlatOverride(override); - addBlockToFlatConfigExport(content, flatOverride); + content = addBlockToFlatConfigExport(content, flatOverride); }); tree.write(fileName, content); @@ -315,7 +315,12 @@ export function addExtendsToLintConfig( const pluginExtends = generatePluginExtendsElement(plugins); let content = tree.read(fileName, 'utf8'); content = addCompatToFlatConfig(content); - tree.write(fileName, addBlockToFlatConfigExport(content, pluginExtends)); + tree.write( + fileName, + addBlockToFlatConfigExport(content, pluginExtends, { + insertAtTheEnd: false, + }) + ); } else { const fileName = joinPathFragments(root, '.eslintrc.json'); updateJson(tree, fileName, (json) => { diff --git a/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts index c7576c6c2a..dc695a4b48 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts @@ -223,7 +223,7 @@ describe('ast-utils', () => { ];`; const result = addCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); + "const { FlatCompat } = require("@eslint/eslintrc"); const js = require("@eslint/js"); const baseConfig = require("../../eslint.config.js"); @@ -262,7 +262,7 @@ describe('ast-utils', () => { ];`; const result = addCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); + "const { FlatCompat } = require("@eslint/eslintrc"); const baseConfig = require("../../eslint.config.js"); const js = require("@eslint/js"); diff --git a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts index 53b6cc8ca5..5918387782 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts @@ -602,7 +602,7 @@ export function addCompatToFlatConfig(content: string) { if (result.includes('const compat = new FlatCompat')) { return result; } - result = addImportToFlatConfig(result, 'FlatCompat', '@eslint/eslintrc'); + result = addImportToFlatConfig(result, ['FlatCompat'], '@eslint/eslintrc'); const index = result.indexOf('module.exports'); return applyChangesToString(result, [ { diff --git a/packages/next/src/generators/application/lib/add-linting.spec.ts b/packages/next/src/generators/application/lib/add-linting.spec.ts index ea210a80cb..1a79152d9a 100644 --- a/packages/next/src/generators/application/lib/add-linting.spec.ts +++ b/packages/next/src/generators/application/lib/add-linting.spec.ts @@ -116,7 +116,7 @@ describe('updateEslint', () => { expect(tree.read(`${schema.appProjectRoot}/eslint.config.js`, 'utf-8')) .toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); + "const { FlatCompat } = require("@eslint/eslintrc"); const js = require("@eslint/js"); const baseConfig = require("../eslint.config.js"); @@ -127,6 +127,7 @@ describe('updateEslint', () => { module.exports = [ + ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), ...baseConfig, { "files": [ @@ -156,7 +157,6 @@ describe('updateEslint', () => { ], rules: {} }, - ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), ...compat.config({ env: { jest: true } }).map(config => ({ ...config, files: [