fix(linter): migrate no-extra-semi rules into user config, out of nx extendable configs (#26011)

This commit is contained in:
James Henry 2024-05-24 17:59:58 +04:00 committed by GitHub
parent 10f97b99bc
commit be6453cfbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 508 additions and 4 deletions

View File

@ -11,6 +11,12 @@
"version": "17.2.6-beta.1",
"description": "Rename workspace rules from @nx/workspace/name to @nx/workspace-name",
"implementation": "./src/migrations/update-17-2-6-rename-workspace-rules/rename-workspace-rules"
},
"update-19-1-0-rename-no-extra-semi": {
"cli": "nx",
"version": "19.1.0-beta.6",
"description": "Migrate no-extra-semi rules into user config, out of nx extendable configs",
"implementation": "./src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi"
}
},
"packageJsonUpdates": {},

View File

@ -57,8 +57,6 @@ export default {
*
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v20 of Nx
*/
'no-extra-semi': 'off',
'@typescript-eslint/no-extra-semi': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',

View File

@ -40,8 +40,6 @@ export default {
*
* TODO(v20): re-evalute these deviations from @typescript-eslint/recommended in v20 of Nx
*/
'no-extra-semi': 'off',
'@typescript-eslint/no-extra-semi': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',

View File

@ -0,0 +1,431 @@
import { Tree, readJson, writeJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import migrate from './migrate-no-extra-semi';
describe('update-19-1-0-migrate-no-extra-semi', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
});
describe('top level config', () => {
it('should not update top level config that does not extend @nx/typescript or @nx/javascript', async () => {
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
rules: {},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"plugins": [
"@nx",
],
"rules": {},
}
`);
});
it('should update top level config that extends @nx/typescript', async () => {
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
extends: ['@nx/typescript'],
rules: {},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"@nx/typescript",
],
"plugins": [
"@nx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
}
`);
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
extends: ['plugin:@nx/typescript'], // alt syntax
rules: {},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"plugin:@nx/typescript",
],
"plugins": [
"@nx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
}
`);
});
it('should update top level config that extends @nx/javascript', async () => {
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
extends: ['@nx/javascript'],
rules: {},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"@nx/javascript",
],
"plugins": [
"@nx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
}
`);
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
extends: ['plugin:@nx/javascript'], // alt syntax
rules: {},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"plugin:@nx/javascript",
],
"plugins": [
"@nx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
}
`);
});
it('should not update top level config that already defines the rules', async () => {
writeJson(tree, '.eslintrc.json', {
plugins: ['@nx'],
extends: ['@nx/typescript'],
rules: {
'no-extra-semi': 'warn', // custom setting
'@typescript-eslint/no-extra-semi': 'warn', // custom setting
},
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"extends": [
"@nx/typescript",
],
"plugins": [
"@nx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "warn",
"no-extra-semi": "warn",
},
}
`);
});
});
describe('overrides', () => {
it('should not update overrides config that does not extend @nx/typescript or @nx/javascript', async () => {
writeJson(tree, 'path/to/.eslintrc.json', {
overrides: [
{
files: ['*.ts'],
plugins: ['@nx'],
rules: {},
},
{
files: ['*.js'],
plugins: ['@nx'],
rules: {},
},
],
});
await migrate(tree);
expect(readJson(tree, 'path/to/.eslintrc.json')).toMatchInlineSnapshot(`
{
"overrides": [
{
"files": [
"*.ts",
],
"plugins": [
"@nx",
],
"rules": {},
},
{
"files": [
"*.js",
],
"plugins": [
"@nx",
],
"rules": {},
},
],
}
`);
});
it('should update overrides config that extends @nx/typescript', async () => {
writeJson(tree, 'path/to/.eslintrc.json', {
overrides: [
{
files: ['*.ts'],
extends: ['@nx/typescript'],
rules: {},
},
{
files: ['*.tsx'],
extends: ['plugin:@nx/typescript'], // alt syntax
rules: {},
},
{
// Should be untouched
files: ['*.js'],
plugins: ['@nx'],
rules: {},
},
],
});
await migrate(tree);
expect(readJson(tree, 'path/to/.eslintrc.json')).toMatchInlineSnapshot(`
{
"overrides": [
{
"extends": [
"@nx/typescript",
],
"files": [
"*.ts",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
},
{
"extends": [
"plugin:@nx/typescript",
],
"files": [
"*.tsx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
},
{
"files": [
"*.js",
],
"plugins": [
"@nx",
],
"rules": {},
},
],
}
`);
});
it('should update overrides config that extends @nx/javascript', async () => {
writeJson(tree, '.eslintrc.json', {
overrides: [
{
files: ['*.js'],
extends: ['@nx/javascript'],
rules: {},
},
{
files: ['*.jsx'],
extends: ['plugin:@nx/javascript'], // alt syntax
rules: {},
},
{
// Should be untouched
files: ['*.js'],
plugins: ['@nx'],
rules: {},
},
],
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"overrides": [
{
"extends": [
"@nx/javascript",
],
"files": [
"*.js",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
},
{
"extends": [
"plugin:@nx/javascript",
],
"files": [
"*.jsx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
},
},
{
"files": [
"*.js",
],
"plugins": [
"@nx",
],
"rules": {},
},
],
}
`);
});
it('should not update overrides config that already defines the rules', async () => {
writeJson(tree, '.eslintrc.json', {
overrides: [
{
files: ['*.ts'],
extends: ['@nx/typescript'],
rules: {
// Custom settings
'@typescript-eslint/no-extra-semi': 'warn',
'no-extra-semi': 'warn',
},
},
{
files: ['*.tsx'],
extends: ['plugin:@nx/typescript'], // alt syntax
rules: {
// Custom settings
'@typescript-eslint/no-extra-semi': 'warn',
'no-extra-semi': 'warn',
},
},
{
files: ['*.js'],
extends: ['@nx/javascript'],
rules: {
// Custom settings
'@typescript-eslint/no-extra-semi': 'warn',
'no-extra-semi': 'warn',
},
},
{
files: ['*.jsx'],
extends: ['plugin:@nx/javascript'], // alt syntax
rules: {
// Custom settings
'@typescript-eslint/no-extra-semi': 'warn',
'no-extra-semi': 'warn',
},
},
],
});
await migrate(tree);
expect(readJson(tree, '.eslintrc.json')).toMatchInlineSnapshot(`
{
"overrides": [
{
"extends": [
"@nx/typescript",
],
"files": [
"*.ts",
],
"rules": {
"@typescript-eslint/no-extra-semi": "warn",
"no-extra-semi": "warn",
},
},
{
"extends": [
"plugin:@nx/typescript",
],
"files": [
"*.tsx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "warn",
"no-extra-semi": "warn",
},
},
{
"extends": [
"@nx/javascript",
],
"files": [
"*.js",
],
"rules": {
"@typescript-eslint/no-extra-semi": "warn",
"no-extra-semi": "warn",
},
},
{
"extends": [
"plugin:@nx/javascript",
],
"files": [
"*.jsx",
],
"rules": {
"@typescript-eslint/no-extra-semi": "warn",
"no-extra-semi": "warn",
},
},
],
}
`);
});
});
});

View File

@ -0,0 +1,71 @@
import {
Tree,
formatFiles,
updateJson,
visitNotIgnoredFiles,
} from '@nx/devkit';
export default async function migrate(tree: Tree): Promise<void> {
visitNotIgnoredFiles(tree, '.', (path) => {
if (!path.endsWith('.eslintrc.json')) {
return;
}
// Cheap check to see if the file contains references to the nx TS or JS eslint configs
const fileContents = tree.read(path, 'utf-8');
if (
!fileContents.includes('@nx/typescript') &&
!fileContents.includes('@nx/javascript')
) {
return;
}
let wasUpdated = false;
updateJson(tree, path, (json) => {
// Update top level config
wasUpdated = addNoExtraSemiExplicitly(json);
// Update overrides
if (json.overrides) {
for (const override of json.overrides) {
const overrideUpdated = addNoExtraSemiExplicitly(override);
wasUpdated = wasUpdated || overrideUpdated;
}
}
return json;
});
if (wasUpdated) {
console.warn(
`NOTE: The configuration for @typescript-eslint/no-extra-semi and no-extra-semi that you were previously inheriting from the Nx eslint-plugin has been explicitly added to your ${path} file.
This is because those rules have been migrated to the https://eslint.style/ project (for stylistic only rules) and will no longer work in v8 of typescript-eslint. Having them explicitly in your config will make it easier for you to handle the transition, either by starting to use the ESLint Stylistic plugin, or removing the rules from your config.`
);
}
});
await formatFiles(tree);
}
/**
* @returns {boolean} whether the json was updated
*/
function addNoExtraSemiExplicitly(json: Record<string, any>): boolean {
let wasUpdated = false;
if (
!json.extends?.includes('@nx/typescript') &&
!json.extends?.includes('plugin:@nx/typescript') &&
!json.extends?.includes('@nx/javascript') &&
!json.extends?.includes('plugin:@nx/javascript')
) {
return wasUpdated;
}
if (!json.rules['@typescript-eslint/no-extra-semi']) {
json.rules['@typescript-eslint/no-extra-semi'] = 'error';
wasUpdated = true;
}
if (!json.rules['no-extra-semi']) {
json.rules['no-extra-semi'] = 'off';
wasUpdated = true;
}
return wasUpdated;
}