diff --git a/packages/eslint-plugin/migrations.json b/packages/eslint-plugin/migrations.json index 7450b5b1338f9..14c0760e50ca0 100644 --- a/packages/eslint-plugin/migrations.json +++ b/packages/eslint-plugin/migrations.json @@ -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": {}, diff --git a/packages/eslint-plugin/src/configs/javascript.ts b/packages/eslint-plugin/src/configs/javascript.ts index d79ac73a712db..6becc9d1f7e14 100644 --- a/packages/eslint-plugin/src/configs/javascript.ts +++ b/packages/eslint-plugin/src/configs/javascript.ts @@ -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', diff --git a/packages/eslint-plugin/src/configs/typescript.ts b/packages/eslint-plugin/src/configs/typescript.ts index a6244addbc27e..58de5a8468f45 100644 --- a/packages/eslint-plugin/src/configs/typescript.ts +++ b/packages/eslint-plugin/src/configs/typescript.ts @@ -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', diff --git a/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.spec.ts b/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.spec.ts new file mode 100644 index 0000000000000..a21d2b763b0b2 --- /dev/null +++ b/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.spec.ts @@ -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", + }, + }, + ], + } + `); + }); + }); +}); diff --git a/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.ts b/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.ts new file mode 100644 index 0000000000000..e4a8cf5ed43e9 --- /dev/null +++ b/packages/eslint-plugin/src/migrations/update-19-1-0-migrate-no-extra-semi/migrate-no-extra-semi.ts @@ -0,0 +1,71 @@ +import { + Tree, + formatFiles, + updateJson, + visitNotIgnoredFiles, +} from '@nx/devkit'; + +export default async function migrate(tree: Tree): Promise { + 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): 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; +}