Skip to content

Commit

Permalink
feat(angular): add migration to remove angular eslint rules removed i…
Browse files Browse the repository at this point in the history
…n v19 (#29214)

Add migration to remove Angular ESLint rules that were removed in v19:

- `@angular-eslint/no-host-metadata-property`
- `@angular-eslint/sort-ngmodule-metadata-arrays`
- `@angular-eslint/prefer-standalone-component`

See Angular ESLint v19 changelog for reference:
https://github.com/angular-eslint/angular-eslint/blob/main/CHANGELOG.md#1900-2024-11-29

<!-- 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` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## 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 #
  • Loading branch information
leosvelperez authored Dec 5, 2024
1 parent 15060e3 commit 2fa3ce2
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 1 deletion.
9 changes: 9 additions & 0 deletions packages/angular/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: {},
},
];
"
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<typeof lintConfigHasOverride>[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;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ function findAllBlocks(source: ts.SourceFile): ts.NodeArray<ts.Node> {
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) &&
Expand Down

0 comments on commit 2fa3ce2

Please sign in to comment.