diff --git a/packages/angular/migrations.json b/packages/angular/migrations.json index a73e327498dfe..aabb5b7630a02 100644 --- a/packages/angular/migrations.json +++ b/packages/angular/migrations.json @@ -314,6 +314,15 @@ }, "description": "Disable the Angular ESLint prefer-standalone rule if not set.", "factory": "./src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone" + }, + "remove-angular-eslint-rules": { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { + "@angular/core": ">=19.0.0" + }, + "description": "Remove Angular ESLint rules that were removed in v19.0.0.", + "factory": "./src/migrations/update-20-2-0/remove-angular-eslint-rules" } }, "packageJsonUpdates": { diff --git a/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.spec.ts b/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.spec.ts new file mode 100644 index 0000000000000..aab8d43539dc5 --- /dev/null +++ b/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.spec.ts @@ -0,0 +1,155 @@ +import { + addProjectConfiguration, + writeJson, + type ProjectConfiguration, + type ProjectGraph, + type Tree, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import migration from './remove-angular-eslint-rules'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: () => Promise.resolve(projectGraph), +})); + +describe('remove-angular-eslint-rules', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + + const projectConfig: ProjectConfiguration = { + name: 'app1', + root: 'apps/app1', + }; + projectGraph = { + dependencies: { + app1: [ + { + source: 'app1', + target: 'npm:@angular/core', + type: 'static', + }, + ], + }, + nodes: { + app1: { + data: projectConfig, + name: 'app1', + type: 'app', + }, + }, + }; + addProjectConfiguration(tree, projectConfig.name, projectConfig); + }); + + describe('.eslintrc.json', () => { + it.each([ + ['@angular-eslint/no-host-metadata-property'], + ['@angular-eslint/sort-ngmodule-metadata-arrays'], + ['@angular-eslint/prefer-standalone-component'], + ])('should remove %s rule', async (rule) => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { [rule]: ['error'] }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')).not.toContain(rule); + }); + + it('should remove multiple rules', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { + '@angular-eslint/no-host-metadata-property': ['error'], + '@angular-eslint/sort-ngmodule-metadata-arrays': ['error'], + '@angular-eslint/prefer-standalone-component': ['error'], + }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.ts"], + "rules": {} + } + ] + } + " + `); + }); + }); + + describe('flat config', () => { + it.each([ + ['@angular-eslint/no-host-metadata-property'], + ['@angular-eslint/sort-ngmodule-metadata-arrays'], + ['@angular-eslint/prefer-standalone-component'], + ])('should remove %s rule', async (rule) => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { '${rule}': ['error'] }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')).not.toContain( + rule + ); + }); + + it('should remove multiple rules', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { + '@angular-eslint/no-host-metadata-property': ['error'], + '@angular-eslint/sort-ngmodule-metadata-arrays': ['error'], + '@angular-eslint/prefer-standalone-component': ['error'], + }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['**/*.ts'], + rules: {}, + }, + ]; + " + `); + }); + }); +}); diff --git a/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.ts b/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.ts new file mode 100644 index 0000000000000..4a41f89a46091 --- /dev/null +++ b/packages/angular/src/migrations/update-20-2-0/remove-angular-eslint-rules.ts @@ -0,0 +1,43 @@ +import { formatFiles, type Tree } from '@nx/devkit'; +import { + isEslintConfigSupported, + lintConfigHasOverride, + updateOverrideInLintConfig, +} from '@nx/eslint/src/generators/utils/eslint-file'; +import { getProjectsFilteredByDependencies } from '../utils/projects'; + +export default async function (tree: Tree) { + const projects = await getProjectsFilteredByDependencies(tree, [ + 'npm:@angular/core', + ]); + + for (const { + project: { root }, + } of projects) { + if (!isEslintConfigSupported(tree, root)) { + // ESLint config is not supported, skip + continue; + } + + removeRule(tree, root, '@angular-eslint/no-host-metadata-property'); + removeRule(tree, root, '@angular-eslint/sort-ngmodule-metadata-arrays'); + removeRule(tree, root, '@angular-eslint/prefer-standalone-component'); + } + + await formatFiles(tree); +} + +function removeRule(tree: Tree, root: string, rule: string) { + const lookup: Parameters[2] = (o) => + !!o.rules?.[rule]; + if (!lintConfigHasOverride(tree, root, lookup, true)) { + // it's not using the rule, skip + return; + } + + // there is an override containing the rule, remove the rule + updateOverrideInLintConfig(tree, root, lookup, (o) => { + delete o.rules[rule]; + return o; + }); +} 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 c86ceefc8d9ff..5cb81e13a371b 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts @@ -63,7 +63,9 @@ function findAllBlocks(source: ts.SourceFile): ts.NodeArray { function isOverride(node: ts.Node): boolean { return ( (ts.isObjectLiteralExpression(node) && - node.properties.some((p) => p.name.getText() === 'files')) || + node.properties.some( + (p) => p.name.getText() === 'files' || p.name.getText() === '"files"' + )) || // detect ...compat.config(...).map(...) (ts.isSpreadElement(node) && ts.isCallExpression(node.expression) &&