From a3c08a9153360371ee09771389299201b3407e00 Mon Sep 17 00:00:00 2001 From: James Henry Date: Fri, 28 May 2021 17:35:34 +0400 Subject: [PATCH] feat(linter): do not set eslint parserOptions.project by default (#5798) --- .eslintrc.json | 3 - .../api-gatsby/generators/application.md | 8 + .../api-next/generators/application.md | 8 + .../api-node/generators/application.md | 8 + .../api-react/generators/application.md | 8 + docs/angular/api-react/generators/library.md | 8 + .../api-workspace/generators/library.md | 8 + docs/map.json | 15 ++ .../node/api-gatsby/generators/application.md | 8 + docs/node/api-next/generators/application.md | 8 + docs/node/api-node/generators/application.md | 8 + docs/node/api-react/generators/application.md | 8 + docs/node/api-react/generators/library.md | 8 + docs/node/api-workspace/generators/library.md | 8 + .../api-gatsby/generators/application.md | 8 + docs/react/api-next/generators/application.md | 8 + docs/react/api-node/generators/application.md | 8 + .../react/api-react/generators/application.md | 8 + docs/react/api-react/generators/library.md | 8 + .../react/api-workspace/generators/library.md | 8 + docs/shared/eslint.md | 108 ++++++++++ .../public/documentation/latest/map.json | 15 ++ .../documentation/latest/shared/eslint.md | 108 ++++++++++ .../public/documentation/previous/map.json | 15 ++ .../documentation/previous/shared/eslint.md | 108 ++++++++++ .../__snapshots__/add-linting.spec.ts.snap | 5 - .../lib/create-eslint-configuration.ts | 21 +- .../src/generators/add-linting/schema.d.ts | 1 + .../src/generators/add-linting/schema.json | 5 + .../convert-tslint-to-eslint.spec.ts.snap | 6 +- .../convert-tslint-to-eslint.spec.ts | 32 ++- .../convert-tslint-to-eslint.ts | 6 + .../application/application.spec.ts | 5 - .../src/schematics/library/library.spec.ts | 5 - .../convert-tslint-to-eslint.spec.ts.snap | 3 - .../convert-tslint-to-eslint.ts | 6 + .../cypress-project/cypress-project.spec.ts | 3 - .../cypress-project/cypress-project.ts | 22 +- .../generators/cypress-project/schema.d.ts | 1 + .../generators/cypress-project/schema.json | 5 + .../eslint-plugin-nx/src/configs/angular.ts | 4 + .../application/application.spec.ts | 5 - .../application/application.spec.ts | 5 - .../generators/application/lib/add-linting.ts | 5 +- .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 5 + packages/linter/migrations.json | 6 + .../src/executors/eslint/lint.impl.spec.ts | 40 +++- .../linter/src/executors/eslint/lint.impl.ts | 32 ++- .../__snapshots__/lint-project.spec.ts.snap | 5 - .../lint-project/lint-project.spec.ts | 4 + .../generators/lint-project/lint-project.ts | 36 +++- ...t-config-if-no-type-checking-rules.spec.ts | 194 ++++++++++++++++++ ...roject-config-if-no-type-checking-rules.ts | 39 ++++ .../project-converter.ts | 13 +- .../utils/rules-requiring-type-checking.ts | 86 ++++++++ .../convert-tslint-to-eslint.spec.ts.snap | 10 - .../convert-tslint-to-eslint.ts | 8 + .../application/application.spec.ts | 5 - .../src/schematics/library/library.spec.ts | 5 - .../application/application.spec.ts | 5 - .../generators/application/lib/add-linting.ts | 6 +- .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 5 + .../application/application.spec.ts | 5 - .../src/generators/application/application.ts | 1 + .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 5 + .../src/generators/library/library.spec.ts | 5 - .../application/application.spec.ts | 5 - .../src/generators/application/application.ts | 5 +- .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 5 + .../src/generators/library/library.spec.ts | 5 - .../react/src/generators/library/library.ts | 5 +- .../react/src/generators/library/schema.d.ts | 1 + .../react/src/generators/library/schema.json | 5 + packages/react/src/utils/lint.ts | 31 ++- .../application/application.spec.ts | 5 - .../src/generators/library/library.spec.ts | 15 -- .../src/generators/library/library.ts | 1 + .../src/generators/library/schema.d.ts | 1 + .../src/generators/library/schema.json | 5 + .../move/lib/update-eslintrc-json.spec.ts | 1 + packages/workspace/src/utils/lint.ts | 27 ++- scripts/depcheck/missing.ts | 2 + 86 files changed, 1146 insertions(+), 158 deletions(-) create mode 100644 docs/shared/eslint.md create mode 100644 nx-dev/nx-dev/public/documentation/latest/shared/eslint.md create mode 100644 nx-dev/nx-dev/public/documentation/previous/shared/eslint.md create mode 100644 packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.spec.ts create mode 100644 packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.ts create mode 100644 packages/linter/src/utils/rules-requiring-type-checking.ts diff --git a/.eslintrc.json b/.eslintrc.json index 303b375bfa..9fab9ebd0d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,6 @@ { "root": true, "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.*?.json" - }, "env": { "node": true }, diff --git a/docs/angular/api-gatsby/generators/application.md b/docs/angular/api-gatsby/generators/application.md index fbb651cd28..ed72dbe620 100644 --- a/docs/angular/api-gatsby/generators/application.md +++ b/docs/angular/api-gatsby/generators/application.md @@ -58,6 +58,14 @@ Generate JavaScript files rather than TypeScript files Type: `string` +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### style Alias(es): s diff --git a/docs/angular/api-next/generators/application.md b/docs/angular/api-next/generators/application.md index ac0a2d21b6..292a04726d 100644 --- a/docs/angular/api-next/generators/application.md +++ b/docs/angular/api-next/generators/application.md @@ -84,6 +84,14 @@ Type: `string` The server script path to be used with next. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/angular/api-node/generators/application.md b/docs/angular/api-node/generators/application.md index e5beb3332a..f187df290d 100644 --- a/docs/angular/api-node/generators/application.md +++ b/docs/angular/api-node/generators/application.md @@ -82,6 +82,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/angular/api-react/generators/application.md b/docs/angular/api-react/generators/application.md index afae5c1d4a..9845367f40 100644 --- a/docs/angular/api-react/generators/application.md +++ b/docs/angular/api-react/generators/application.md @@ -126,6 +126,14 @@ Type: `boolean` Generate application with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/angular/api-react/generators/library.md b/docs/angular/api-react/generators/library.md index 9cde8c90fd..2b8d267765 100644 --- a/docs/angular/api-react/generators/library.md +++ b/docs/angular/api-react/generators/library.md @@ -134,6 +134,14 @@ Type: `boolean` Generate library with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/angular/api-workspace/generators/library.md b/docs/angular/api-workspace/generators/library.md index c56ebec859..ff8fcffd4e 100644 --- a/docs/angular/api-workspace/generators/library.md +++ b/docs/angular/api-workspace/generators/library.md @@ -98,6 +98,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipBabelrc Default: `false` diff --git a/docs/map.json b/docs/map.json index b644efca91..f37b080a8a 100644 --- a/docs/map.json +++ b/docs/map.json @@ -1022,6 +1022,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "Nx 7 => Nx 8", "id": "nx7-to-nx8" @@ -2076,6 +2081,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "JavaScript and TypeScript", "id": "js-and-ts", @@ -3084,6 +3094,11 @@ "name": "Using Nx at Enterprises", "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" + }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" } ] } diff --git a/docs/node/api-gatsby/generators/application.md b/docs/node/api-gatsby/generators/application.md index 240428c26f..7269ac5bbd 100644 --- a/docs/node/api-gatsby/generators/application.md +++ b/docs/node/api-gatsby/generators/application.md @@ -58,6 +58,14 @@ Generate JavaScript files rather than TypeScript files Type: `string` +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### style Alias(es): s diff --git a/docs/node/api-next/generators/application.md b/docs/node/api-next/generators/application.md index 7fe8a93eed..ebdc892804 100644 --- a/docs/node/api-next/generators/application.md +++ b/docs/node/api-next/generators/application.md @@ -84,6 +84,14 @@ Type: `string` The server script path to be used with next. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/node/api-node/generators/application.md b/docs/node/api-node/generators/application.md index 5ed7f18cd4..7978f16169 100644 --- a/docs/node/api-node/generators/application.md +++ b/docs/node/api-node/generators/application.md @@ -82,6 +82,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/node/api-react/generators/application.md b/docs/node/api-react/generators/application.md index 5e92254b46..15bc5d395c 100644 --- a/docs/node/api-react/generators/application.md +++ b/docs/node/api-react/generators/application.md @@ -126,6 +126,14 @@ Type: `boolean` Generate application with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/node/api-react/generators/library.md b/docs/node/api-react/generators/library.md index cad2390688..1020cdb2f6 100644 --- a/docs/node/api-react/generators/library.md +++ b/docs/node/api-react/generators/library.md @@ -134,6 +134,14 @@ Type: `boolean` Generate library with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/node/api-workspace/generators/library.md b/docs/node/api-workspace/generators/library.md index 867fb33495..44fd48eafb 100644 --- a/docs/node/api-workspace/generators/library.md +++ b/docs/node/api-workspace/generators/library.md @@ -98,6 +98,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipBabelrc Default: `false` diff --git a/docs/react/api-gatsby/generators/application.md b/docs/react/api-gatsby/generators/application.md index 240428c26f..7269ac5bbd 100644 --- a/docs/react/api-gatsby/generators/application.md +++ b/docs/react/api-gatsby/generators/application.md @@ -58,6 +58,14 @@ Generate JavaScript files rather than TypeScript files Type: `string` +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### style Alias(es): s diff --git a/docs/react/api-next/generators/application.md b/docs/react/api-next/generators/application.md index 7fe8a93eed..ebdc892804 100644 --- a/docs/react/api-next/generators/application.md +++ b/docs/react/api-next/generators/application.md @@ -84,6 +84,14 @@ Type: `string` The server script path to be used with next. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/react/api-node/generators/application.md b/docs/react/api-node/generators/application.md index 5ed7f18cd4..7978f16169 100644 --- a/docs/react/api-node/generators/application.md +++ b/docs/react/api-node/generators/application.md @@ -82,6 +82,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/react/api-react/generators/application.md b/docs/react/api-react/generators/application.md index 5e92254b46..15bc5d395c 100644 --- a/docs/react/api-react/generators/application.md +++ b/docs/react/api-react/generators/application.md @@ -126,6 +126,14 @@ Type: `boolean` Generate application with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/react/api-react/generators/library.md b/docs/react/api-react/generators/library.md index cad2390688..1020cdb2f6 100644 --- a/docs/react/api-react/generators/library.md +++ b/docs/react/api-react/generators/library.md @@ -134,6 +134,14 @@ Type: `boolean` Generate library with routes. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipFormat Default: `false` diff --git a/docs/react/api-workspace/generators/library.md b/docs/react/api-workspace/generators/library.md index 867fb33495..44fd48eafb 100644 --- a/docs/react/api-workspace/generators/library.md +++ b/docs/react/api-workspace/generators/library.md @@ -98,6 +98,14 @@ Type: `boolean` Use pascal case file names. +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + ### skipBabelrc Default: `false` diff --git a/docs/shared/eslint.md b/docs/shared/eslint.md new file mode 100644 index 0000000000..d379ea46c6 --- /dev/null +++ b/docs/shared/eslint.md @@ -0,0 +1,108 @@ +# Using ESLint in Nx Workspaces + +## Rules requiring type information + +ESLint is powerful linter by itself, able to work on the syntax of your source files and assert things about based on the rules you configure. It gets even more powerful, however, when TypeScript type-checker is layered on top of it when analyzing TypeScript files, which is something that `@typescript-eslint` allows us to do. + +By default, Nx sets up your ESLint configs with performance in mind - we want your linting to run as fast as possible. Because creating the necessary so called TypeScript `Program`s required to create the type-checker behind the scenes is relatively expensive compared to pure syntax analysis, you should only configure the `parserOptions.project` option in your project's `.eslintrc.json` when you need to use rules requiring type information (and you should not configure `parserOptions.project` in your workspace's root `.eslintrc.json`). + +Let's take an example of an ESLint config that Nx might generate for you out of the box for a Next.js project called `tuskdesk`: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Here we do _not_ have `parserOptions.project`, which is appropriate because we are not leveraging any rules which require type information. + +If we now come in and add a rule which does require type information, for example `@typescript-eslint/await-thenable`, our config will look as follows: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + // This rule requires the TypeScript type checker to be present when it runs + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Now if we try and run `nx lint tuskdesk` we will get an error + +``` +> nx run tuskdesk:lint + +Linting "tuskdesk"... + + Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have `parserOptions.project` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config `apps/tuskdesk/.eslintrc.json` + +``` + +The solution is to update our config once more, this time to set `parserOptions.project` to appropriately point at our various tsconfig.json files which belong to our project: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + // We set parserOptions.project for the project to allow TypeScript to create the type-checker behind the scenes when we run linting + "parserOptions": { + "project": ["apps/tuskdesk/tsconfig.*?.json"] + }, + "rules": { + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +And that's it! Now any rules requiring type information will run correctly when we run `nx lint tuskdesk`. + +> NOTE: As well as adapting the path to match your project's real path, please be aware that if you apply the above to a **Next.js** application, you should change the glob pattern at the end to be `tsconfig(.*)?.json`. E.g. if `tuskdesk` had been a Next.js app, we would have written: `"project": ["apps/tuskdesk/tsconfig(.*)?.json"]` diff --git a/nx-dev/nx-dev/public/documentation/latest/map.json b/nx-dev/nx-dev/public/documentation/latest/map.json index a961fd05d6..9bb9732516 100644 --- a/nx-dev/nx-dev/public/documentation/latest/map.json +++ b/nx-dev/nx-dev/public/documentation/latest/map.json @@ -1003,6 +1003,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "Nx 7 => Nx 8", "id": "nx7-to-nx8" @@ -2033,6 +2038,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "JavaScript and TypeScript", "id": "js-and-ts", @@ -3017,6 +3027,11 @@ "name": "Using Nx at Enterprises", "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" + }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" } ] } diff --git a/nx-dev/nx-dev/public/documentation/latest/shared/eslint.md b/nx-dev/nx-dev/public/documentation/latest/shared/eslint.md new file mode 100644 index 0000000000..d379ea46c6 --- /dev/null +++ b/nx-dev/nx-dev/public/documentation/latest/shared/eslint.md @@ -0,0 +1,108 @@ +# Using ESLint in Nx Workspaces + +## Rules requiring type information + +ESLint is powerful linter by itself, able to work on the syntax of your source files and assert things about based on the rules you configure. It gets even more powerful, however, when TypeScript type-checker is layered on top of it when analyzing TypeScript files, which is something that `@typescript-eslint` allows us to do. + +By default, Nx sets up your ESLint configs with performance in mind - we want your linting to run as fast as possible. Because creating the necessary so called TypeScript `Program`s required to create the type-checker behind the scenes is relatively expensive compared to pure syntax analysis, you should only configure the `parserOptions.project` option in your project's `.eslintrc.json` when you need to use rules requiring type information (and you should not configure `parserOptions.project` in your workspace's root `.eslintrc.json`). + +Let's take an example of an ESLint config that Nx might generate for you out of the box for a Next.js project called `tuskdesk`: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Here we do _not_ have `parserOptions.project`, which is appropriate because we are not leveraging any rules which require type information. + +If we now come in and add a rule which does require type information, for example `@typescript-eslint/await-thenable`, our config will look as follows: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + // This rule requires the TypeScript type checker to be present when it runs + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Now if we try and run `nx lint tuskdesk` we will get an error + +``` +> nx run tuskdesk:lint + +Linting "tuskdesk"... + + Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have `parserOptions.project` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config `apps/tuskdesk/.eslintrc.json` + +``` + +The solution is to update our config once more, this time to set `parserOptions.project` to appropriately point at our various tsconfig.json files which belong to our project: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + // We set parserOptions.project for the project to allow TypeScript to create the type-checker behind the scenes when we run linting + "parserOptions": { + "project": ["apps/tuskdesk/tsconfig.*?.json"] + }, + "rules": { + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +And that's it! Now any rules requiring type information will run correctly when we run `nx lint tuskdesk`. + +> NOTE: As well as adapting the path to match your project's real path, please be aware that if you apply the above to a **Next.js** application, you should change the glob pattern at the end to be `tsconfig(.*)?.json`. E.g. if `tuskdesk` had been a Next.js app, we would have written: `"project": ["apps/tuskdesk/tsconfig(.*)?.json"]` diff --git a/nx-dev/nx-dev/public/documentation/previous/map.json b/nx-dev/nx-dev/public/documentation/previous/map.json index 1cba380bf4..fe60a498f6 100644 --- a/nx-dev/nx-dev/public/documentation/previous/map.json +++ b/nx-dev/nx-dev/public/documentation/previous/map.json @@ -1048,6 +1048,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "Nx 7 => Nx 8", "id": "nx7-to-nx8" @@ -2122,6 +2127,11 @@ "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" + }, { "name": "JavaScript and TypeScript", "id": "js-and-ts", @@ -3146,6 +3156,11 @@ "name": "Using Nx at Enterprises", "id": "monorepo-nx-enterprise", "file": "shared/monorepo-nx-enterprise" + }, + { + "name": "Using ESLint in Nx Workspaces", + "id": "eslint", + "file": "shared/eslint" } ] } diff --git a/nx-dev/nx-dev/public/documentation/previous/shared/eslint.md b/nx-dev/nx-dev/public/documentation/previous/shared/eslint.md new file mode 100644 index 0000000000..d379ea46c6 --- /dev/null +++ b/nx-dev/nx-dev/public/documentation/previous/shared/eslint.md @@ -0,0 +1,108 @@ +# Using ESLint in Nx Workspaces + +## Rules requiring type information + +ESLint is powerful linter by itself, able to work on the syntax of your source files and assert things about based on the rules you configure. It gets even more powerful, however, when TypeScript type-checker is layered on top of it when analyzing TypeScript files, which is something that `@typescript-eslint` allows us to do. + +By default, Nx sets up your ESLint configs with performance in mind - we want your linting to run as fast as possible. Because creating the necessary so called TypeScript `Program`s required to create the type-checker behind the scenes is relatively expensive compared to pure syntax analysis, you should only configure the `parserOptions.project` option in your project's `.eslintrc.json` when you need to use rules requiring type information (and you should not configure `parserOptions.project` in your workspace's root `.eslintrc.json`). + +Let's take an example of an ESLint config that Nx might generate for you out of the box for a Next.js project called `tuskdesk`: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Here we do _not_ have `parserOptions.project`, which is appropriate because we are not leveraging any rules which require type information. + +If we now come in and add a rule which does require type information, for example `@typescript-eslint/await-thenable`, our config will look as follows: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + // This rule requires the TypeScript type checker to be present when it runs + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +Now if we try and run `nx lint tuskdesk` we will get an error + +``` +> nx run tuskdesk:lint + +Linting "tuskdesk"... + + Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have `parserOptions.project` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config `apps/tuskdesk/.eslintrc.json` + +``` + +The solution is to update our config once more, this time to set `parserOptions.project` to appropriately point at our various tsconfig.json files which belong to our project: + +**apps/tuskdesk/.eslintrc.json** + +```jsonc +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + // We set parserOptions.project for the project to allow TypeScript to create the type-checker behind the scenes when we run linting + "parserOptions": { + "project": ["apps/tuskdesk/tsconfig.*?.json"] + }, + "rules": { + "@typescript-eslint/await-thenable": "error" + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} +``` + +And that's it! Now any rules requiring type information will run correctly when we run `nx lint tuskdesk`. + +> NOTE: As well as adapting the path to match your project's real path, please be aware that if you apply the above to a **Next.js** application, you should change the glob pattern at the end to be `tsconfig(.*)?.json`. E.g. if `tuskdesk` had been a Next.js app, we would have written: `"project": ["apps/tuskdesk/tsconfig(.*)?.json"]` diff --git a/packages/angular/src/generators/add-linting/__snapshots__/add-linting.spec.ts.snap b/packages/angular/src/generators/add-linting/__snapshots__/add-linting.spec.ts.snap index ceabae4291..5e81a2a787 100644 --- a/packages/angular/src/generators/add-linting/__snapshots__/add-linting.spec.ts.snap +++ b/packages/angular/src/generators/add-linting/__snapshots__/add-linting.spec.ts.snap @@ -17,11 +17,6 @@ Object { "files": Array [ "*.ts", ], - "parserOptions": Object { - "project": Array [ - "apps/ng-app1/tsconfig.*?.json", - ], - }, "rules": Object { "@angular-eslint/component-selector": Array [ "error", diff --git a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts index d4ef3d70ff..875115fb56 100644 --- a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts +++ b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts @@ -21,9 +21,24 @@ export function createEsLintConfiguration( 'plugin:@nrwl/nx/angular', 'plugin:@angular-eslint/template/process-inline-templates', ], - parserOptions: { - project: [`${options.projectRoot}/tsconfig.*?.json`], - }, + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: [`${options.projectRoot}/tsconfig.*?.json`], + }, rules: { '@angular-eslint/directive-selector': [ 'error', diff --git a/packages/angular/src/generators/add-linting/schema.d.ts b/packages/angular/src/generators/add-linting/schema.d.ts index a1c83dd046..cc712b4aab 100644 --- a/packages/angular/src/generators/add-linting/schema.d.ts +++ b/packages/angular/src/generators/add-linting/schema.d.ts @@ -2,4 +2,5 @@ export interface AddLintingGeneratorSchema { projectName: string; projectRoot: string; prefix: string; + setParserOptionsProject?: boolean; } diff --git a/packages/angular/src/generators/add-linting/schema.json b/packages/angular/src/generators/add-linting/schema.json index ca2ff2e8c4..1b3c88ede3 100644 --- a/packages/angular/src/generators/add-linting/schema.json +++ b/packages/angular/src/generators/add-linting/schema.json @@ -17,6 +17,11 @@ "projectRoot": { "type": "string", "description": "The path to the root of the selected project." + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "additionalProperties": false, diff --git a/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap b/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap index 2597157eb3..8e0b268d97 100644 --- a/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap +++ b/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap @@ -332,6 +332,7 @@ Object { "type": "attribute", }, ], + "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-empty-interface": "error", }, }, @@ -659,11 +660,6 @@ Object { "files": Array [ "*.ts", ], - "parserOptions": Object { - "project": Array [ - "libs/angular-lib-1/tsconfig.*?.json", - ], - }, "plugins": Array [ "@angular-eslint/eslint-plugin", "@typescript-eslint", diff --git a/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.spec.ts b/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.spec.ts index f924121502..d3faf28248 100644 --- a/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.spec.ts +++ b/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.spec.ts @@ -79,7 +79,20 @@ function mockFindReportedConfiguration(_, pathToTslintJson) { case 'tslint.json': return exampleRootTslintJson.tslintPrintConfigResult; case appProjectTSLintJsonPath: - return projectTslintJsonData.tslintPrintConfigResult; + return { + /** + * Add in an example of rule which requires type-checking so we can test + * that parserOptions.project is appropriately preserved in the final + * config in this case. + */ + rules: { + ...projectTslintJsonData.tslintPrintConfigResult.rules, + 'await-promise': { + ruleArguments: [], + ruleSeverity: 'error', + }, + }, + }; case libProjectTSLintJsonPath: return projectTslintJsonData.tslintPrintConfigResult; default: @@ -167,11 +180,18 @@ describe('convert-tslint-to-eslint', () => { /** * Existing tslint.json file for the app project */ - writeJson( - host, - 'apps/angular-app-1/tslint.json', - projectTslintJsonData.raw - ); + writeJson(host, 'apps/angular-app-1/tslint.json', { + ...projectTslintJsonData.raw, + rules: { + ...projectTslintJsonData.raw.rules, + /** + * Add in an example of rule which requires type-checking so we can test + * that parserOptions.project is appropriately preserved in the final + * config in this case. + */ + 'await-promise': true, + }, + }); /** * Existing tslint.json file for the lib project */ diff --git a/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts b/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts index 02615c3664..05cbb9ea02 100755 --- a/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts +++ b/packages/angular/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts @@ -33,6 +33,12 @@ export async function conversionGenerator( projectName, projectRoot: projectConfig.root, prefix: (projectConfig as any).prefix || 'app', + /** + * We set the parserOptions.project config just in case the converted config uses + * rules which require type-checking. Later in the conversion we check if it actually + * does and remove the config again if it doesn't, so that it is most efficient. + */ + setParserOptionsProject: true, }); }, }); diff --git a/packages/angular/src/schematics/application/application.spec.ts b/packages/angular/src/schematics/application/application.spec.ts index 1d7c564c9c..0aa6605f9d 100644 --- a/packages/angular/src/schematics/application/application.spec.ts +++ b/packages/angular/src/schematics/application/application.spec.ts @@ -404,11 +404,6 @@ describe('app', () => { "files": Array [ "*.ts", ], - "parserOptions": Object { - "project": Array [ - "apps/my-app/tsconfig.*?.json", - ], - }, "rules": Object { "@angular-eslint/component-selector": Array [ "error", diff --git a/packages/angular/src/schematics/library/library.spec.ts b/packages/angular/src/schematics/library/library.spec.ts index e082d64eec..2eda92b915 100644 --- a/packages/angular/src/schematics/library/library.spec.ts +++ b/packages/angular/src/schematics/library/library.spec.ts @@ -1249,11 +1249,6 @@ describe('lib', () => { "files": Array [ "*.ts", ], - "parserOptions": Object { - "project": Array [ - "libs/my-lib/tsconfig.*?.json", - ], - }, "rules": Object { "@angular-eslint/component-selector": Array [ "error", diff --git a/packages/cypress/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap b/packages/cypress/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap index d6494f74db..d020751241 100644 --- a/packages/cypress/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap +++ b/packages/cypress/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap @@ -262,9 +262,6 @@ Object { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": "apps/e2e-app-1/tsconfig.*?.json", - }, "rules": Object {}, }, Object { diff --git a/packages/cypress/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts b/packages/cypress/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts index 56445e584f..02ace8fe3f 100755 --- a/packages/cypress/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts +++ b/packages/cypress/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts @@ -34,6 +34,12 @@ export async function conversionGenerator( linter: 'eslint', projectName, projectRoot: projectConfig.root, + /** + * We set the parserOptions.project config just in case the converted config uses + * rules which require type-checking. Later in the conversion we check if it actually + * does and remove the config again if it doesn't, so that it is most efficient. + */ + setParserOptionsProject: true, } as CypressProjectSchema); }, }); diff --git a/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts b/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts index 14b8dc7769..6cff96a2d3 100644 --- a/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts +++ b/packages/cypress/src/generators/cypress-project/cypress-project.spec.ts @@ -343,9 +343,6 @@ describe('schematic:cypress-project', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": "apps/my-app-e2e/tsconfig.*?.json", - }, "rules": Object {}, }, Object { diff --git a/packages/cypress/src/generators/cypress-project/cypress-project.ts b/packages/cypress/src/generators/cypress-project/cypress-project.ts index 6e15a3a158..903dabe2b4 100644 --- a/packages/cypress/src/generators/cypress-project/cypress-project.ts +++ b/packages/cypress/src/generators/cypress-project/cypress-project.ts @@ -90,6 +90,7 @@ export async function addLinter(host: Tree, options: CypressProjectSchema) { eslintFilePatterns: [ `${options.projectRoot}/**/*.${options.js ? 'js' : '{js,ts}'}`, ], + setParserOptionsProject: options.setParserOptionsProject, }); if (!options.linter || options.linter !== Linter.EsLint) { @@ -112,9 +113,24 @@ export async function addLinter(host: Tree, options: CypressProjectSchema) { */ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: { - project: `${options.projectRoot}/tsconfig.*?.json`, - }, + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: `${options.projectRoot}/tsconfig.*?.json`, + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to diff --git a/packages/cypress/src/generators/cypress-project/schema.d.ts b/packages/cypress/src/generators/cypress-project/schema.d.ts index 8ed33c4457..fdff50b834 100644 --- a/packages/cypress/src/generators/cypress-project/schema.d.ts +++ b/packages/cypress/src/generators/cypress-project/schema.d.ts @@ -7,4 +7,5 @@ export interface Schema { linter?: Linter; js?: boolean; skipFormat?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/cypress/src/generators/cypress-project/schema.json b/packages/cypress/src/generators/cypress-project/schema.json index aac61115c3..46d6645949 100644 --- a/packages/cypress/src/generators/cypress-project/schema.json +++ b/packages/cypress/src/generators/cypress-project/schema.json @@ -40,6 +40,11 @@ "description": "Skip formatting files", "type": "boolean", "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": ["name"] diff --git a/packages/eslint-plugin-nx/src/configs/angular.ts b/packages/eslint-plugin-nx/src/configs/angular.ts index 2ede14b420..1b197235ad 100644 --- a/packages/eslint-plugin-nx/src/configs/angular.ts +++ b/packages/eslint-plugin-nx/src/configs/angular.ts @@ -33,5 +33,9 @@ export default { ? ['plugin:@angular-eslint/recommended--extra'] : []), ], + parserOptions: { + // Unset the default value for parserOptions.project that is found in earlier versions of @angular-eslint + project: [], + }, rules: {}, }; diff --git a/packages/express/src/generators/application/application.spec.ts b/packages/express/src/generators/application/application.spec.ts index 045558be7a..6fb083755e 100644 --- a/packages/express/src/generators/application/application.spec.ts +++ b/packages/express/src/generators/application/application.spec.ts @@ -53,11 +53,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-node-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/gatsby/src/generators/application/application.spec.ts b/packages/gatsby/src/generators/application/application.spec.ts index fb951c2a63..5c60427dd4 100644 --- a/packages/gatsby/src/generators/application/application.spec.ts +++ b/packages/gatsby/src/generators/application/application.spec.ts @@ -320,11 +320,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/gatsby/src/generators/application/lib/add-linting.ts b/packages/gatsby/src/generators/application/lib/add-linting.ts index 8c174c24d4..b1175c2637 100644 --- a/packages/gatsby/src/generators/application/lib/add-linting.ts +++ b/packages/gatsby/src/generators/application/lib/add-linting.ts @@ -21,7 +21,10 @@ export async function addLinting(host: Tree, options: NormalizedSchema) { skipFormat: true, }); - const reactEslintJson = createReactEslintJson(options.projectRoot); + const reactEslintJson = createReactEslintJson( + options.projectRoot, + options.setParserOptionsProject + ); updateJson( host, diff --git a/packages/gatsby/src/generators/application/schema.d.ts b/packages/gatsby/src/generators/application/schema.d.ts index 0f52774bb7..b5bb5a6744 100644 --- a/packages/gatsby/src/generators/application/schema.d.ts +++ b/packages/gatsby/src/generators/application/schema.d.ts @@ -8,4 +8,5 @@ export interface Schema { unitTestRunner?: 'jest' | 'none'; e2eTestRunner?: 'cypress' | 'none'; js?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/gatsby/src/generators/application/schema.json b/packages/gatsby/src/generators/application/schema.json index c24bbb0962..8bf8de2767 100644 --- a/packages/gatsby/src/generators/application/schema.json +++ b/packages/gatsby/src/generators/application/schema.json @@ -84,6 +84,11 @@ "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files", "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": ["name"] diff --git a/packages/linter/migrations.json b/packages/linter/migrations.json index 8ee530ce92..b5d9cb0d7c 100644 --- a/packages/linter/migrations.json +++ b/packages/linter/migrations.json @@ -40,6 +40,12 @@ "version": "11.5.0-beta.0", "description": "Update project .eslintrc.json files to always use project level tsconfigs", "factory": "./src/migrations/update-11-5-0/always-use-project-level-tsconfigs-with-eslint" + }, + "remove-eslint-project-config-if-no-type-checking-rules": { + "cli": "nx", + "version": "12.4.0-beta.0", + "description": "Remove ESLint parserOptions.project config if no rules requiring type-checking are in use", + "factory": "./src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules" } }, "packageJsonUpdates": { diff --git a/packages/linter/src/executors/eslint/lint.impl.spec.ts b/packages/linter/src/executors/eslint/lint.impl.spec.ts index 020a98d05d..a7cecd0e61 100644 --- a/packages/linter/src/executors/eslint/lint.impl.spec.ts +++ b/packages/linter/src/executors/eslint/lint.impl.spec.ts @@ -42,7 +42,7 @@ function createValidRunBuilderOptions( ): Schema { return { lintFilePatterns: [], - eslintConfig: './.eslintrc.json', + eslintConfig: null, fix: true, cache: true, cacheLocation: 'cacheLocation1', @@ -73,16 +73,24 @@ describe('Linter Builder', () => { beforeEach(() => { MockESLint.version = VALID_ESLINT_VERSION; mockReports = [{ results: [], usedDeprecatedRules: [] }]; + const projectName = 'proj'; mockContext = { - projectName: 'proj', + projectName, root: '/root', cwd: '/root', workspace: { version: 2, - projects: {}, + projects: { + [projectName]: { + root: `apps/${projectName}`, + sourceRoot: `apps/${projectName}/src`, + targets: {}, + }, + }, }, isVerbose: false, }; + mockLint.mockImplementation(() => mockReports); }); afterAll(() => { @@ -224,6 +232,32 @@ describe('Linter Builder', () => { ); }); + it('should intercept the error from `@typescript-eslint` regarding missing parserServices and provide a more detailed user-facing message', async () => { + setupMocks(); + + mockLint.mockImplementation(() => { + throw new Error( + `Error while loading rule '@typescript-eslint/await-thenable': You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.` + ); + }); + + await lintExecutor( + createValidRunBuilderOptions({ + lintFilePatterns: ['includedFile1'], + format: 'json', + silent: false, + }), + mockContext + ); + expect(console.error).toHaveBeenCalledWith( + ` +Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config \`apps/proj/.eslintrc.json\` + +Please see https://nx.dev/latest/guides/eslint for full guidance on how to resolve this issue. +` + ); + }); + it('should log if there are no warnings or errors', async () => { mockReports = [ { diff --git a/packages/linter/src/executors/eslint/lint.impl.ts b/packages/linter/src/executors/eslint/lint.impl.ts index 303c1c83ce..ec7c328a65 100644 --- a/packages/linter/src/executors/eslint/lint.impl.ts +++ b/packages/linter/src/executors/eslint/lint.impl.ts @@ -46,7 +46,37 @@ export default async function run( ? resolve(systemRoot, options.eslintConfig) : undefined; - let lintResults: ESLint.LintResult[] = await lint(eslintConfigPath, options); + let lintResults: ESLint.LintResult[] = []; + + try { + lintResults = await lint(eslintConfigPath, options); + } catch (err) { + if ( + err.message.includes( + 'You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser' + ) + ) { + let eslintConfigPathForError = `for ${projectName}`; + if (context.workspace?.projects?.[projectName]?.root) { + const { root } = context.workspace.projects[projectName]; + eslintConfigPathForError = `\`${root}/.eslintrc.json\``; + } + + console.error(` +Error: You have attempted to use a lint rule which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${ + eslintConfigPath || eslintConfigPathForError + } + +Please see https://nx.dev/latest/guides/eslint for full guidance on how to resolve this issue. +`); + + return { + success: false, + }; + } + // If some unexpected error, rethrow + throw err; + } if (lintResults.length === 0) { throw new Error('Invalid lint configuration. Nothing to lint.'); diff --git a/packages/linter/src/generators/lint-project/__snapshots__/lint-project.spec.ts.snap b/packages/linter/src/generators/lint-project/__snapshots__/lint-project.spec.ts.snap index 43f811db42..dc9ca0bd94 100644 --- a/packages/linter/src/generators/lint-project/__snapshots__/lint-project.spec.ts.snap +++ b/packages/linter/src/generators/lint-project/__snapshots__/lint-project.spec.ts.snap @@ -16,11 +16,6 @@ exports[`@nrwl/linter:lint-project --linter eslint should generate a eslint conf \\"*.js\\", \\"*.jsx\\" ], - \\"parserOptions\\": { - \\"project\\": [ - \\"libs/test-lib/tsconfig.*?.json\\" - ] - }, \\"rules\\": {} }, { diff --git a/packages/linter/src/generators/lint-project/lint-project.spec.ts b/packages/linter/src/generators/lint-project/lint-project.spec.ts index ed93df0400..9f41e70ba8 100644 --- a/packages/linter/src/generators/lint-project/lint-project.spec.ts +++ b/packages/linter/src/generators/lint-project/lint-project.spec.ts @@ -31,6 +31,7 @@ describe('@nrwl/linter:lint-project', () => { linter: Linter.EsLint, eslintFilePatterns: ['**/*.ts'], project: 'test-lib', + setParserOptionsProject: false, }); expect( @@ -44,6 +45,7 @@ describe('@nrwl/linter:lint-project', () => { linter: Linter.EsLint, eslintFilePatterns: ['**/*.ts'], project: 'test-lib', + setParserOptionsProject: false, }); const projectConfig = readProjectConfiguration(tree, 'test-lib'); @@ -67,6 +69,7 @@ describe('@nrwl/linter:lint-project', () => { linter: Linter.TsLint, tsConfigPaths: ['tsconfig.json'], project: 'test-lib', + setParserOptionsProject: false, }); expect( @@ -80,6 +83,7 @@ describe('@nrwl/linter:lint-project', () => { linter: Linter.TsLint, tsConfigPaths: ['tsconfig.json'], project: 'test-lib', + setParserOptionsProject: false, }); const projectConfig = readProjectConfiguration(tree, 'test-lib'); diff --git a/packages/linter/src/generators/lint-project/lint-project.ts b/packages/linter/src/generators/lint-project/lint-project.ts index 42927f1d05..089bcc96f2 100644 --- a/packages/linter/src/generators/lint-project/lint-project.ts +++ b/packages/linter/src/generators/lint-project/lint-project.ts @@ -17,6 +17,7 @@ interface LintProjectOptions { eslintFilePatterns?: string[]; tsConfigPaths?: string[]; skipFormat: boolean; + setParserOptionsProject?: boolean; } function createTsLintConfiguration( @@ -35,7 +36,8 @@ function createTsLintConfiguration( function createEsLintConfiguration( tree: Tree, - projectConfig: ProjectConfiguration + projectConfig: ProjectConfiguration, + setParserOptionsProject: boolean ) { writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { extends: [`${offsetFromRoot(projectConfig.root)}.eslintrc.json`], @@ -44,14 +46,24 @@ function createEsLintConfiguration( overrides: [ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: { - /** - * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs - * behind the scenes during lint runs, we need to make sure the project is configured to use its - * own specific tsconfigs, and not fall back to the ones in the root of the workspace. - */ - project: [`${projectConfig.root}/tsconfig.*?.json`], - }, + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !setParserOptionsProject + ? undefined + : { + project: [`${projectConfig.root}/tsconfig.*?.json`], + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to @@ -86,7 +98,11 @@ export async function lintProjectGenerator( lintFilePatterns: options.eslintFilePatterns, }, }; - createEsLintConfiguration(tree, projectConfig); + createEsLintConfiguration( + tree, + projectConfig, + options.setParserOptionsProject + ); } else { projectConfig.targets['lint'] = { executor: '@angular-devkit/build-angular:tslint', diff --git a/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.spec.ts b/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.spec.ts new file mode 100644 index 0000000000..2a07c7fde1 --- /dev/null +++ b/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.spec.ts @@ -0,0 +1,194 @@ +import { addProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import remoteESLintProjectConfigIfNoTypeCheckingRules from './remove-eslint-project-config-if-no-type-checking-rules'; + +const KNOWN_RULE_REQUIRING_TYPE_CHECKING = '@typescript-eslint/await-thenable'; + +describe('Remove ESLint parserOptions.project config if no rules requiring type-checking are in use', () => { + let tree: Tree; + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'react-app', { + root: 'apps/react-app', + sourceRoot: 'apps/react-app/src', + projectType: 'application', + targets: {}, + }); + addProjectConfiguration(tree, 'workspace-lib', { + root: 'libs/workspace-lib', + sourceRoot: 'libs/workspace-lib/src', + projectType: 'library', + targets: {}, + }); + addProjectConfiguration(tree, 'some-lib', { + root: 'libs/some-lib', + sourceRoot: 'libs/some-lib/src', + projectType: 'library', + targets: {}, + }); + }); + + it('should not update any configs if the root .eslintrc.json contains at least one rule requiring type-checking', async () => { + const rootEslintConfig = { + root: true, + ignorePatterns: ['**/*'], + plugins: ['@nrwl/nx'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: { + '@nrwl/nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, + ], + }, + ], + }, + }, + { + files: ['*.ts', '*.tsx'], + extends: ['plugin:@nrwl/nx/typescript'], + rules: { + [KNOWN_RULE_REQUIRING_TYPE_CHECKING]: 'error', + }, + }, + { + files: ['*.js', '*.jsx'], + extends: ['plugin:@nrwl/nx/javascript'], + rules: {}, + }, + ], + }; + tree.write('.eslintrc.json', JSON.stringify(rootEslintConfig)); + + const projectEslintConfig1 = { + extends: '../../../.eslintrc.json', + ignorePatterns: ['!**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + parserOptions: { + project: 'some-path-to-tsconfig.json', + }, + rules: {}, + }, + ], + }; + tree.write( + 'apps/react-app/.eslintrc.json', + JSON.stringify(projectEslintConfig1) + ); + + const projectEslintConfig2 = { + extends: '../../../.eslintrc.json', + ignorePatterns: ['!**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + parserOptions: { + project: 'some-path-to-tsconfig.json', + }, + rules: {}, + }, + ], + }; + tree.write( + 'libs/workspace-lib/.eslintrc.json', + JSON.stringify(projectEslintConfig2) + ); + + await remoteESLintProjectConfigIfNoTypeCheckingRules(tree); + + // No change + expect(readJson(tree, 'apps/react-app/.eslintrc.json')).toEqual( + projectEslintConfig1 + ); + + // No change + expect(readJson(tree, 'libs/workspace-lib/.eslintrc.json')).toEqual( + projectEslintConfig1 + ); + }); + + it('should remove the parserOptions.project from any project .eslintrc.json files that do not contain any rules requiring type-checking', async () => { + // Root doesn't contain any rules requiring type-checking + const rootEslintConfig = { + root: true, + ignorePatterns: ['**/*'], + plugins: ['@nrwl/nx'], + overrides: [], + }; + tree.write('.eslintrc.json', JSON.stringify(rootEslintConfig)); + + const projectEslintConfig1 = { + extends: '../../../.eslintrc.json', + ignorePatterns: ['!**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + parserOptions: { + project: 'some-path-to-tsconfig.json', + }, + rules: { + [KNOWN_RULE_REQUIRING_TYPE_CHECKING]: 'error', + }, + }, + ], + }; + tree.write( + 'apps/react-app/.eslintrc.json', + JSON.stringify(projectEslintConfig1) + ); + + const projectEslintConfig2 = { + extends: '../../../.eslintrc.json', + ignorePatterns: ['!**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + parserOptions: { + project: 'some-path-to-tsconfig.json', + }, + rules: { + // No rules requiring type-checking + }, + }, + ], + }; + tree.write( + 'libs/workspace-lib/.eslintrc.json', + JSON.stringify(projectEslintConfig2) + ); + + await remoteESLintProjectConfigIfNoTypeCheckingRules(tree); + + // No change - uses rule requiring type-checking + expect(readJson(tree, 'apps/react-app/.eslintrc.json')).toEqual( + projectEslintConfig1 + ); + + // Updated - no more parserOptions.project + expect(readJson(tree, 'libs/workspace-lib/.eslintrc.json')) + .toMatchInlineSnapshot(` + Object { + "extends": "../../../.eslintrc.json", + "ignorePatterns": Array [ + "!**/*", + ], + "overrides": Array [ + Object { + "files": Array [ + "*.ts", + "*.tsx", + ], + "rules": Object {}, + }, + ], + } + `); + }); +}); diff --git a/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.ts b/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.ts new file mode 100644 index 0000000000..24f43a6abc --- /dev/null +++ b/packages/linter/src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules.ts @@ -0,0 +1,39 @@ +import { + formatFiles, + getProjects, + readJson, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { join } from 'path'; +import { + hasRulesRequiringTypeChecking, + removeParserOptionsProjectIfNotRequired, +} from '../../utils/rules-requiring-type-checking'; + +function updateProjectESLintConfigs(host: Tree) { + const projects = getProjects(host); + projects.forEach((p) => { + const eslintConfigPath = join(p.root, '.eslintrc.json'); + if (!host.exists(eslintConfigPath)) { + return; + } + return updateJson( + host, + eslintConfigPath, + removeParserOptionsProjectIfNotRequired + ); + }); +} + +export default async function removeESLintProjectConfigIfNoTypeCheckingRules( + host: Tree +) { + // If the root level config uses at least one rule requiring type-checking, do not migrate any project configs + const rootESLintConfig = readJson(host, '.eslintrc.json'); + if (hasRulesRequiringTypeChecking(rootESLintConfig)) { + return; + } + updateProjectESLintConfigs(host); + await formatFiles(host); +} diff --git a/packages/linter/src/utils/convert-tslint-to-eslint/project-converter.ts b/packages/linter/src/utils/convert-tslint-to-eslint/project-converter.ts index 2a7343d55e..47068c3cec 100644 --- a/packages/linter/src/utils/convert-tslint-to-eslint/project-converter.ts +++ b/packages/linter/src/utils/convert-tslint-to-eslint/project-converter.ts @@ -17,6 +17,7 @@ import { updateWorkspaceConfiguration, } from '@nrwl/devkit'; import type { Linter } from 'eslint'; +import { removeParserOptionsProjectIfNotRequired } from '../rules-requiring-type-checking'; import { convertTSLintDisableCommentsForProject } from './convert-to-eslint-config'; import { convertTSLintConfig, @@ -226,7 +227,11 @@ export class ProjectConverter { }); } json.overrides = deduplicateOverrides(json.overrides); - return json; + /** + * Remove the parserOptions.project config if it is not required for the final config, + * so that lint runs can be as fast and efficient as possible. + */ + return removeParserOptionsProjectIfNotRequired(json); }); /** @@ -332,7 +337,11 @@ export class ProjectConverter { * updating the config file. */ const finalJson = applyPackageSpecificModifications(json); - return finalJson; + /** + * Remove the parserOptions.project config if it is not required for the final config, + * so that lint runs can be as fast and efficient as possible. + */ + return removeParserOptionsProjectIfNotRequired(finalJson); }); /** diff --git a/packages/linter/src/utils/rules-requiring-type-checking.ts b/packages/linter/src/utils/rules-requiring-type-checking.ts new file mode 100644 index 0000000000..79cefea9b8 --- /dev/null +++ b/packages/linter/src/utils/rules-requiring-type-checking.ts @@ -0,0 +1,86 @@ +import type { Linter } from 'eslint'; + +// Cache the resolved rules from node_modules +let knownRulesRequiringTypeChecking: string[] | null = null; + +function resolveKnownRulesRequiringTypeChecking(): string[] | null { + if (knownRulesRequiringTypeChecking) { + return knownRulesRequiringTypeChecking; + } + try { + const { rules } = require('@typescript-eslint/eslint-plugin'); + const rulesRequiringTypeInfo = Object.entries(rules) + .map(([ruleName, config]) => { + if ((config as any).meta?.docs?.requiresTypeChecking) { + return `@typescript-eslint/${ruleName}`; + } + return null; + }) + .filter(Boolean); + return rulesRequiringTypeInfo; + } catch (err) { + console.log(err); + return null; + } +} + +export function hasRulesRequiringTypeChecking( + eslintConfig: Linter.Config +): boolean { + knownRulesRequiringTypeChecking = resolveKnownRulesRequiringTypeChecking(); + if (!knownRulesRequiringTypeChecking) { + /** + * If (unexpectedly) known rules requiring type checking could not be resolved, + * default to assuming that the rules are in use to align most closely with Nx + * ESLint configs to date. + */ + return true; + } + const allRulesInConfig = getAllRulesInConfig(eslintConfig); + return allRulesInConfig.some((rule) => + knownRulesRequiringTypeChecking.includes(rule) + ); +} + +export function removeParserOptionsProjectIfNotRequired( + json: Linter.Config +): Linter.Config { + // At least one rule requiring type-checking is in use, do not migrate the config + if (hasRulesRequiringTypeChecking(json)) { + return json; + } + removeProjectParserOptionFromConfig(json); + return json; +} + +function getAllRulesInConfig(json: Linter.Config): string[] { + let allRules = json.rules ? Object.keys(json.rules) : []; + if (json.overrides?.length > 0) { + for (const override of json.overrides) { + if (override.rules) { + allRules = [...allRules, ...Object.keys(override.rules)]; + } + } + } + return allRules; +} + +function removeProjectParserOptionFromConfig(json: Linter.Config): void { + delete json.parserOptions?.project; + // If parserOptions is left empty by this removal, also clean up the whole object + if (json.parserOptions && Object.keys(json.parserOptions).length === 0) { + delete json.parserOptions; + } + if (json.overrides) { + for (const override of json.overrides) { + delete override.parserOptions?.project; + // If parserOptions is left empty by this removal, also clean up the whole object + if ( + override.parserOptions && + Object.keys(override.parserOptions).length === 0 + ) { + delete override.parserOptions; + } + } + } +} diff --git a/packages/nest/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap b/packages/nest/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap index c14facb023..d09d83e2fc 100644 --- a/packages/nest/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap +++ b/packages/nest/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap @@ -260,11 +260,6 @@ Object { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/nest-app-1/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { @@ -551,11 +546,6 @@ Object { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/nest-lib-1/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/nest/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts b/packages/nest/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts index e725cace6d..34780b1c9b 100755 --- a/packages/nest/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts +++ b/packages/nest/src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint.ts @@ -39,6 +39,12 @@ export async function conversionGenerator( * delegating to the external (more generic) generators below. */ const js = false; + /** + * We set the parserOptions.project config just in case the converted config uses + * rules which require type-checking. Later in the conversion we check if it actually + * does and remove the config again if it doesn't, so that it is most efficient. + */ + const setParserOptionsProject = true; if (projectConfig.projectType === 'application') { await addLintingToApplication(host, { @@ -46,6 +52,7 @@ export async function conversionGenerator( name: projectName, appProjectRoot: projectConfig.root, js, + setParserOptionsProject, } as AddLintForApplicationSchema); } @@ -55,6 +62,7 @@ export async function conversionGenerator( name: projectName, projectRoot: projectConfig.root, js, + setParserOptionsProject, } as AddLintForLibrarySchema); } }, diff --git a/packages/nest/src/schematics/application/application.spec.ts b/packages/nest/src/schematics/application/application.spec.ts index d1fda51638..b91bad3c74 100644 --- a/packages/nest/src/schematics/application/application.spec.ts +++ b/packages/nest/src/schematics/application/application.spec.ts @@ -38,11 +38,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-node-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/nest/src/schematics/library/library.spec.ts b/packages/nest/src/schematics/library/library.spec.ts index 28dcafa7f8..1ba4bc82e8 100644 --- a/packages/nest/src/schematics/library/library.spec.ts +++ b/packages/nest/src/schematics/library/library.spec.ts @@ -222,11 +222,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 4ef862a5fe..00b21ee9b8 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -283,11 +283,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-app/tsconfig(.*)?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index 959856d149..c799f08249 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -25,11 +25,15 @@ export async function addLinting( }); if (options.linter === Linter.EsLint) { - const reactEslintJson = createReactEslintJson(options.appProjectRoot); + const reactEslintJson = createReactEslintJson( + options.appProjectRoot, + options.setParserOptionsProject + ); updateJson( host, joinPathFragments(options.appProjectRoot, '.eslintrc.json'), () => { + // Only set parserOptions.project if it already exists (defined by options.setParserOptionsProject) if (reactEslintJson.overrides?.[0].parserOptions?.project) { reactEslintJson.overrides[0].parserOptions.project = [ `${options.appProjectRoot}/tsconfig(.*)?.json`, diff --git a/packages/next/src/generators/application/schema.d.ts b/packages/next/src/generators/application/schema.d.ts index ab8f723b47..9aabdb93ff 100644 --- a/packages/next/src/generators/application/schema.d.ts +++ b/packages/next/src/generators/application/schema.d.ts @@ -13,4 +13,5 @@ export interface Schema { linter?: Linter; skipWorkspaceJson?: boolean; js?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/next/src/generators/application/schema.json b/packages/next/src/generators/application/schema.json index ef489758de..ce586d5c16 100644 --- a/packages/next/src/generators/application/schema.json +++ b/packages/next/src/generators/application/schema.json @@ -104,6 +104,11 @@ "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": [] diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index 66429af240..a7a3b1fb05 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -132,11 +132,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-node-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index ec576fb12c..78072fafbd 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -174,6 +174,7 @@ export async function addLintingToApplication( `${options.appProjectRoot}/**/*.${options.js ? 'js' : 'ts'}`, ], skipFormat: true, + setParserOptionsProject: options.setParserOptionsProject, }); return lintTask; diff --git a/packages/node/src/generators/application/schema.d.ts b/packages/node/src/generators/application/schema.d.ts index d05927ab6b..cec497cfac 100644 --- a/packages/node/src/generators/application/schema.d.ts +++ b/packages/node/src/generators/application/schema.d.ts @@ -12,4 +12,5 @@ export interface Schema { babelJest?: boolean; js?: boolean; pascalCaseFiles?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/node/src/generators/application/schema.json b/packages/node/src/generators/application/schema.json index 2685c4221a..5879ebdba0 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -63,6 +63,11 @@ "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": [] diff --git a/packages/node/src/generators/library/library.spec.ts b/packages/node/src/generators/library/library.spec.ts index 1bca0fb527..98fdffee5e 100644 --- a/packages/node/src/generators/library/library.spec.ts +++ b/packages/node/src/generators/library/library.spec.ts @@ -119,11 +119,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 0839e46a2b..3f36730fd4 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -390,11 +390,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index 6dbe60140b..fd7d308ad2 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -38,7 +38,10 @@ async function addLinting(host: Tree, options: NormalizedSchema) { }); tasks.push(lintTask); - const reactEslintJson = createReactEslintJson(options.appProjectRoot); + const reactEslintJson = createReactEslintJson( + options.appProjectRoot, + options.setParserOptionsProject + ); updateJson( host, diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts index 149c69ea82..43f3c7f771 100644 --- a/packages/react/src/generators/application/schema.d.ts +++ b/packages/react/src/generators/application/schema.d.ts @@ -18,6 +18,7 @@ export interface Schema { js?: boolean; globalCss?: boolean; strict?: boolean; + setParserOptionsProject?: boolean; } export interface NormalizedSchema extends Schema { diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json index a78cfb0b10..0378af1b86 100644 --- a/packages/react/src/generators/application/schema.json +++ b/packages/react/src/generators/application/schema.json @@ -140,6 +140,11 @@ "type": "boolean", "description": "Creates an application with stricter type checking and build optimization options.", "default": true + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": [] diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index 0625002ba8..f91443ad70 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -145,11 +145,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index f126cc801e..6cc090e8f2 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -150,7 +150,10 @@ async function addLinting(host: Tree, options: NormalizedSchema) { return; } - const reactEslintJson = createReactEslintJson(options.projectRoot); + const reactEslintJson = createReactEslintJson( + options.projectRoot, + options.setParserOptionsProject + ); updateJson( host, diff --git a/packages/react/src/generators/library/schema.d.ts b/packages/react/src/generators/library/schema.d.ts index aa745e787b..35a93ef068 100644 --- a/packages/react/src/generators/library/schema.d.ts +++ b/packages/react/src/generators/library/schema.d.ts @@ -20,4 +20,5 @@ export interface Schema { js?: boolean; globalCss?: boolean; strict?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/react/src/generators/library/schema.json b/packages/react/src/generators/library/schema.json index d339bf78d0..ec7bc4360f 100644 --- a/packages/react/src/generators/library/schema.json +++ b/packages/react/src/generators/library/schema.json @@ -145,6 +145,11 @@ "type": "boolean", "description": "Whether to enable tsconfig strict mode or not.", "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": ["name"] diff --git a/packages/react/src/utils/lint.ts b/packages/react/src/utils/lint.ts index ba15b331b4..69dff978db 100644 --- a/packages/react/src/utils/lint.ts +++ b/packages/react/src/utils/lint.ts @@ -17,7 +17,10 @@ export const extraEslintDependencies = { }, }; -export const createReactEslintJson = (projectRoot: string): Linter.Config => ({ +export const createReactEslintJson = ( + projectRoot: string, + setParserOptionsProject: boolean +): Linter.Config => ({ extends: [ 'plugin:@nrwl/nx/react', `${offsetFromRoot(projectRoot)}.eslintrc.json`, @@ -26,14 +29,24 @@ export const createReactEslintJson = (projectRoot: string): Linter.Config => ({ overrides: [ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: { - /** - * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs - * behind the scenes during lint runs, we need to make sure the project is configured to use its - * own specific tsconfigs, and not fall back to the ones in the root of the workspace. - */ - project: [`${projectRoot}/tsconfig.*?.json`], - }, + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !setParserOptionsProject + ? undefined + : { + project: [`${projectRoot}/tsconfig.*?.json`], + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index d0ee1e2cf4..e9fbb2acb0 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -83,11 +83,6 @@ describe('app', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "apps/my-app/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/workspace/src/generators/library/library.spec.ts b/packages/workspace/src/generators/library/library.spec.ts index 1c6561179c..b0cdb8e39f 100644 --- a/packages/workspace/src/generators/library/library.spec.ts +++ b/packages/workspace/src/generators/library/library.spec.ts @@ -321,11 +321,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { @@ -394,11 +389,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-dir/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { @@ -678,11 +668,6 @@ describe('lib', () => { "*.js", "*.jsx", ], - "parserOptions": Object { - "project": Array [ - "libs/my-dir/my-lib/tsconfig.*?.json", - ], - }, "rules": Object {}, }, Object { diff --git a/packages/workspace/src/generators/library/library.ts b/packages/workspace/src/generators/library/library.ts index 26b5901c85..de07b2204f 100644 --- a/packages/workspace/src/generators/library/library.ts +++ b/packages/workspace/src/generators/library/library.ts @@ -73,6 +73,7 @@ export function addLint( eslintFilePatterns: [ `${options.projectRoot}/**/*.${options.js ? 'js' : 'ts'}`, ], + setParserOptionsProject: options.setParserOptionsProject, }); } diff --git a/packages/workspace/src/generators/library/schema.d.ts b/packages/workspace/src/generators/library/schema.d.ts index dc0aca3894..04a8181310 100644 --- a/packages/workspace/src/generators/library/schema.d.ts +++ b/packages/workspace/src/generators/library/schema.d.ts @@ -18,4 +18,5 @@ export interface Schema { strict?: boolean; skipBabelrc?: boolean; buildable?: boolean; + setParserOptionsProject?: boolean; } diff --git a/packages/workspace/src/generators/library/schema.json b/packages/workspace/src/generators/library/schema.json index c1f5348e9c..d85024353e 100644 --- a/packages/workspace/src/generators/library/schema.json +++ b/packages/workspace/src/generators/library/schema.json @@ -91,6 +91,11 @@ "type": "boolean", "default": false, "description": "Generate a buildable library." + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false } }, "required": ["name"] diff --git a/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts b/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts index 7f38308feb..3b8fa03485 100644 --- a/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-eslintrc-json.spec.ts @@ -65,6 +65,7 @@ describe('updateEslint', () => { await libraryGenerator(tree, { name: 'my-lib', linter: Linter.EsLint, + setParserOptionsProject: true, }); // This step is usually handled elsewhere diff --git a/packages/workspace/src/utils/lint.ts b/packages/workspace/src/utils/lint.ts index 1f8562ebb6..952cb01e91 100644 --- a/packages/workspace/src/utils/lint.ts +++ b/packages/workspace/src/utils/lint.ts @@ -55,6 +55,7 @@ interface AddLintFileOptions { dependencies: { [key: string]: string }; devDependencies: { [key: string]: string }; }; + setParserOptionsProject?: boolean; } export function addLintFiles( projectRoot: string, @@ -163,14 +164,24 @@ export function addLintFiles( overrides: [ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: { - /** - * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs - * behind the scenes during lint runs, we need to make sure the project is configured to use its - * own specific tsconfigs, and not fall back to the ones in the root of the workspace. - */ - project: [`${projectRoot}/tsconfig.*?.json`], - }, + /** + * NOTE: We no longer set parserOptions.project by default when creating new projects. + * + * We have observed that users rarely add rules requiring type-checking to their Nx workspaces, and therefore + * do not actually need the capabilites which parserOptions.project provides. When specifying parserOptions.project, + * typescript-eslint needs to create full TypeScript Programs for you. When omitting it, it can perform a simple + * parse (and AST tranformation) of the source files it encounters during a lint run, which is much faster and much + * less memory intensive. + * + * In the rare case that users attempt to add rules requiring type-checking to their setup later on (and haven't set + * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you + * and provide feedback to the user. + */ + parserOptions: !options.setParserOptionsProject + ? undefined + : { + project: [`${projectRoot}/tsconfig.*?.json`], + }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index 67f760df72..d59a75d8a6 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -37,6 +37,8 @@ const IGNORE_MATCHES = { '@angular-devkit/architect', // Installed and uninstalled dynamically when the conversion generator runs 'tslint-to-eslint-config', + // Resolved from the end user's own workspace installation dynamically + '@typescript-eslint/eslint-plugin', ], next: [ '@angular-devkit/architect',