diff --git a/packages/eslint/src/generators/init/init-migration.ts b/packages/eslint/src/generators/init/init-migration.ts index f3dc738abdbfd..ea199ff126b7c 100644 --- a/packages/eslint/src/generators/init/init-migration.ts +++ b/packages/eslint/src/generators/init/init-migration.ts @@ -23,6 +23,7 @@ import { generateSpreadElement, removeCompatExtends, removePlugin, + removePredefinedConfigs, } from '../utils/flat-config/ast-utils'; import { hasEslintPlugin } from '../utils/plugin'; import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file'; @@ -152,6 +153,11 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) { 'plugin:@nrwl/typescript', 'plugin:@nrwl/javascript', ]); + config = removePredefinedConfigs(config, '@nx/eslint-plugin', 'nx', [ + 'flat/base', + 'flat/typescript', + 'flat/javascript', + ]); tree.write(projectEslintPath, config); } else { updateJson(tree, projectEslintPath, (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 80435f89b466f..d620a52373eba 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 @@ -7,10 +7,13 @@ import { generateFlatOverride, generatePluginExtendsElementWithCompatFixup, removeCompatExtends, + removeImportFromFlatConfig, removeOverridesFromLintConfig, removePlugin, + removePredefinedConfigs, replaceOverride, } from './ast-utils'; +import { stripIndents } from '@nx/devkit'; describe('ast-utils', () => { const printer = ts.createPrinter(); @@ -341,6 +344,32 @@ describe('ast-utils', () => { }); }); + describe('removeImportFromFlatConfig', () => { + it('should remove existing import from config if the var name matches', () => { + const content = stripIndents` + const nx = require("@nx/eslint-plugin"); + const thisShouldRemain = require("@nx/eslint-plugin"); + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + playwright.configs['flat/recommended'], + ]; + `; + const result = removeImportFromFlatConfig( + content, + 'nx', + '@nx/eslint-plugin' + ); + expect(result).toMatchInlineSnapshot(` + " + const thisShouldRemain = require("@nx/eslint-plugin"); + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + playwright.configs['flat/recommended'], + ];" + `); + }); + }); + describe('addCompatToFlatConfig', () => { it('should add compat to config', () => { const content = `const baseConfig = require("../../eslint.config.js"); @@ -966,6 +995,66 @@ describe('ast-utils', () => { }); }); + describe('removePredefinedConfigs', () => { + it('should remove config objects and import', () => { + const content = stripIndents` + const nx = require("@nx/eslint-plugin"); + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + ...nx.config['flat/base'], + ...nx.config['flat/typescript'], + ...nx.config['flat/javascript'], + playwright.configs['flat/recommended'], + ]; + `; + + const result = removePredefinedConfigs( + content, + '@nx/eslint-plugin', + 'nx', + ['flat/base', 'flat/typescript', 'flat/javascript'] + ); + + expect(result).toMatchInlineSnapshot(` + " + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + playwright.configs['flat/recommended'], + ];" + `); + }); + + it('should keep configs that are not in the list', () => { + const content = stripIndents` + const nx = require("@nx/eslint-plugin"); + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + ...nx.config['flat/base'], + ...nx.config['flat/typescript'], + ...nx.config['flat/javascript'], + ...nx.config['flat/react'], + playwright.configs['flat/recommended'], + ]; + `; + + const result = removePredefinedConfigs( + content, + '@nx/eslint-plugin', + 'nx', + ['flat/base', 'flat/typescript', 'flat/javascript'] + ); + + expect(result).toMatchInlineSnapshot(` + "const nx = require("@nx/eslint-plugin"); + const playwright = require('eslint-plugin-playwright'); + module.exports = [ + ...nx.config['flat/react'], + playwright.configs['flat/recommended'], + ];" + `); + }); + }); + describe('generatePluginExtendsElementWithCompatFixup', () => { it('should return spread element with fixupConfigRules call wrapping the extended plugin', () => { const result = generatePluginExtendsElementWithCompatFixup('my-plugin'); 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 9a4d4dbc8f539..573d523151826 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts @@ -315,6 +315,50 @@ export function addImportToFlatConfig( ]); } +/** + * Remove an import from flat config + */ +export function removeImportFromFlatConfig( + content: string, + variable: string, + imp: string +): string { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + + const changes: StringChange[] = []; + + ts.forEachChild(source, (node) => { + // we can only combine object binding patterns + if ( + ts.isVariableStatement(node) && + ts.isVariableDeclaration(node.declarationList.declarations[0]) && + ts.isIdentifier(node.declarationList.declarations[0].name) && + node.declarationList.declarations[0].name.getText() === variable && + ts.isCallExpression(node.declarationList.declarations[0].initializer) && + node.declarationList.declarations[0].initializer.expression.getText() === + 'require' && + ts.isStringLiteral( + node.declarationList.declarations[0].initializer.arguments[0] + ) && + node.declarationList.declarations[0].initializer.arguments[0].text === imp + ) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + } + }); + + return applyChangesToString(content, changes); +} + /** * Injects new ts.expression to the end of the module.exports array. */ @@ -570,6 +614,52 @@ export function removeCompatExtends( return applyChangesToString(content, changes); } +export function removePredefinedConfigs( + content: string, + moduleImport: string, + moduleVariable: string, + configs: string[] +): string { + const source = ts.createSourceFile( + '', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + const changes: StringChange[] = []; + let removeImport = true; + findAllBlocks(source).forEach((node) => { + if ( + ts.isSpreadElement(node) && + ts.isElementAccessExpression(node.expression) && + ts.isPropertyAccessExpression(node.expression.expression) && + ts.isIdentifier(node.expression.expression.expression) && + node.expression.expression.expression.getText() === moduleVariable && + ts.isStringLiteral(node.expression.argumentExpression) + ) { + const config = node.expression.argumentExpression.getText(); + // Check the text without quotes + if (configs.includes(config.substring(1, config.length - 1))) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos + 1, // trailing comma + }); + } else { + // If there is still a config used, do not remove import + removeImport = false; + } + } + }); + + let updated = applyChangesToString(content, changes); + if (removeImport) { + updated = removeImportFromFlatConfig(updated, moduleVariable, moduleImport); + } + return updated; +} + /** * Add plugins block to the top of the export blocks */