feat(linter): convert-tslint-to-eslint generators (#4943)

* feat(linter): convert-tslint-to-eslint generators

* fix(core): remove generators in collection for ng and nest

* fix(core): update tao to support mixed generators and schematics

* fix(core): update tao to support mixed generators and schematics

* fix(core): address some PR feedback

* fix(core): fix snapshots after syncing up with master

* feat(core): store user preference for removeTSLintIfNoMoreTSLintTargets

* fix(linter): unit tests

* feat(core): apply root tslint.json conversion to root .eslintrc.json
This commit is contained in:
James Henry 2021-03-19 21:41:13 +04:00 committed by GitHub
parent 4eebad2a6d
commit 00dec221e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 5863 additions and 153 deletions

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `angular.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the Angular project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the Angular project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `angular.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/nest:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the NestJS project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the NestJS project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -443,6 +443,11 @@
"id": "upgrade-module",
"file": "angular/api-angular/generators/upgrade-module"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "angular/api-angular/generators/convert-tslint-to-eslint"
},
{
"name": "package executor",
"id": "package",
@ -747,6 +752,11 @@
"name": "service generator",
"id": "service",
"file": "angular/api-nest/generators/service"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "angular/api-nest/generators/convert-tslint-to-eslint"
}
]
},
@ -1466,6 +1476,11 @@
"id": "ngrx",
"file": "react/api-angular/generators/ngrx"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "react/api-angular/generators/convert-tslint-to-eslint"
},
{
"name": "stories generator",
"id": "stories",
@ -1796,6 +1811,11 @@
"name": "service generator",
"id": "service",
"file": "react/api-nest/generators/service"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "react/api-nest/generators/convert-tslint-to-eslint"
}
]
},
@ -2488,6 +2508,11 @@
"id": "upgrade-module",
"file": "node/api-angular/generators/upgrade-module"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "node/api-angular/generators/convert-tslint-to-eslint"
},
{
"name": "package executor",
"id": "package",
@ -2791,6 +2816,11 @@
"name": "service generator",
"id": "service",
"file": "node/api-nest/generators/service"
},
{
"name": "convert-tslint-to-eslint",
"id": "convert-tslint-to-eslint",
"file": "node/api-nest/generators/convert-tslint-to-eslint"
}
]
},

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the Angular project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the Angular project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/nest:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the NestJS project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the NestJS project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/angular:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the Angular project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the Angular project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -0,0 +1,47 @@
# convert-tslint-to-eslint
Convert a project from TSLint to ESLint
## Usage
```bash
nx generate convert-tslint-to-eslint ...
```
By default, Nx will search for `convert-tslint-to-eslint` in the default collection provisioned in `workspace.json`.
You can specify the collection explicitly as follows:
```bash
nx g @nrwl/nest:convert-tslint-to-eslint ...
```
Show what will be generated without writing to disk:
```bash
nx g convert-tslint-to-eslint ... --dry-run
```
### Examples
Convert the NestJS project `myapp` from TSLint to ESLint:
```bash
nx g convert-tslint-to-eslint myapp
```
## Options
### project
Type: `string`
The name of the NestJS project to convert.
### removeTSLintIfNoMoreTSLintTargets
Default: `true`
Type: `boolean`
If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration

View File

@ -79,7 +79,7 @@
},
"nest": {
"tags": [],
"implicitDependencies": ["node"]
"implicitDependencies": ["node", "linter"]
},
"linter": {
"tags": [],

View File

@ -249,6 +249,7 @@
"tsickle": "^0.38.1",
"tslib": "^2.0.0",
"tslint": "6.1.3",
"tslint-to-eslint-config": "2.2.0",
"typescript": "4.0.5",
"url-loader": "^3.0.0",
"verdaccio": "^4.11.1",

View File

@ -95,6 +95,26 @@
"schema": "./src/schematics/move/schema.json",
"aliases": ["mv"],
"description": "Move an Angular application or library to another folder"
},
"add-linting": {
"factory": "./src/schematics/add-linting/add-linting",
"schema": "./src/schematics/add-linting/schema.json",
"description": "Add linting configuration to an Angular project",
"hidden": true
},
"convert-tslint-to-eslint": {
"factory": "./src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint#conversionSchematic",
"schema": "./src/generators/convert-tslint-to-eslint/schema.json",
"description": "Convert a project from TSLint to ESLint"
}
},
"generators": {
"convert-tslint-to-eslint": {
"factory": "./src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint#conversionGenerator",
"schema": "./src/generators/convert-tslint-to-eslint/schema.json",
"description": "Convert a project from TSLint to ESLint"
}
}
}

View File

@ -1 +1,2 @@
export * from './src/schematics/generators';
export * from './src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint';

View File

@ -0,0 +1,679 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convert-tslint-to-eslint should work for Angular applications 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@angular-eslint/eslint-plugin": "~1.0.0",
"@angular-eslint/eslint-plugin-template": "~1.0.0",
"@angular-eslint/template-parser": "~1.0.0",
"@nrwl/eslint-plugin-nx": "*",
"@nrwl/linter": "*",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"eslint": "7.10.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "latest",
},
"name": "test-name",
}
`;
exports[`convert-tslint-to-eslint should work for Angular applications 2`] = `
Object {
"prefix": "angular-app",
"projectType": "application",
"root": "apps/angular-app-1",
"targets": Object {
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/angular-app-1/src/**/*.ts",
"apps/angular-app-1/src/**/*.html",
],
},
},
},
}
`;
exports[`convert-tslint-to-eslint should work for Angular applications 3`] = `
Object {
"ignorePatterns": Array [
"**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {
"@nrwl/nx/enforce-module-boundaries": Array [
"error",
Object {
"allow": Array [
"@nx-example/shared/product/data/testing",
],
"depConstraints": Array [
Object {
"onlyDependOnLibsWithTags": Array [
"type:feature",
"type:ui",
],
"sourceTag": "type:app",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:ui",
"type:data",
"type:types",
"type:state",
],
"sourceTag": "type:feature",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:types",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:state",
"type:types",
"type:data",
],
"sourceTag": "type:state",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:data",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:e2e-utils",
],
"sourceTag": "type:e2e",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
"type:ui",
],
"sourceTag": "type:ui",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:products",
"scope:shared",
],
"sourceTag": "scope:products",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:cart",
"scope:shared",
],
"sourceTag": "scope:cart",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/typescript",
],
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/javascript",
],
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
],
"plugins": Array [
"eslint-plugin-import",
],
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "app",
"style": "camelCase",
"type": "attribute",
},
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"_buffer",
"_counters",
"_timers",
"_groupDepth",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
Object {
"files": Array [
"*.html",
],
"rules": Object {
"@angular-eslint/template/banana-in-box": "error",
"@angular-eslint/template/no-negated-async": "error",
},
},
],
"plugins": Array [
"@nrwl/nx",
],
"root": true,
}
`;
exports[`convert-tslint-to-eslint should work for Angular applications 4`] = `
Object {
"extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"apps/angular-app-1/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "camelCase",
"type": "attribute",
},
],
"@typescript-eslint/no-empty-interface": "error",
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {
"@angular-eslint/template/banana-in-box": "error",
},
},
],
}
`;
exports[`convert-tslint-to-eslint should work for Angular libraries 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@angular-eslint/eslint-plugin": "~1.0.0",
"@angular-eslint/eslint-plugin-template": "~1.0.0",
"@angular-eslint/template-parser": "~1.0.0",
"@nrwl/eslint-plugin-nx": "*",
"@nrwl/linter": "*",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"eslint": "7.10.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "latest",
},
"name": "test-name",
}
`;
exports[`convert-tslint-to-eslint should work for Angular libraries 2`] = `
Object {
"prefix": "angular-app",
"projectType": "library",
"root": "libs/angular-lib-1",
"targets": Object {
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"libs/angular-lib-1/src/**/*.ts",
"libs/angular-lib-1/src/**/*.html",
],
},
},
},
}
`;
exports[`convert-tslint-to-eslint should work for Angular libraries 3`] = `
Object {
"ignorePatterns": Array [
"**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {
"@nrwl/nx/enforce-module-boundaries": Array [
"error",
Object {
"allow": Array [
"@nx-example/shared/product/data/testing",
],
"depConstraints": Array [
Object {
"onlyDependOnLibsWithTags": Array [
"type:feature",
"type:ui",
],
"sourceTag": "type:app",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:ui",
"type:data",
"type:types",
"type:state",
],
"sourceTag": "type:feature",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:types",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:state",
"type:types",
"type:data",
],
"sourceTag": "type:state",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:data",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:e2e-utils",
],
"sourceTag": "type:e2e",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
"type:ui",
],
"sourceTag": "type:ui",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:products",
"scope:shared",
],
"sourceTag": "scope:products",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:cart",
"scope:shared",
],
"sourceTag": "scope:cart",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/typescript",
],
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/javascript",
],
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
],
"plugins": Array [
"eslint-plugin-import",
],
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "app",
"style": "camelCase",
"type": "attribute",
},
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"_buffer",
"_counters",
"_timers",
"_groupDepth",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
Object {
"files": Array [
"*.html",
],
"rules": Object {
"@angular-eslint/template/banana-in-box": "error",
"@angular-eslint/template/no-negated-async": "error",
},
},
],
"plugins": Array [
"@nrwl/nx",
],
"root": true,
}
`;
exports[`convert-tslint-to-eslint should work for Angular libraries 4`] = `
Object {
"extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"libs/angular-lib-1/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "camelCase",
"type": "attribute",
},
],
"@typescript-eslint/no-empty-interface": "error",
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {
"@angular-eslint/template/banana-in-box": "error",
},
},
],
}
`;

View File

@ -0,0 +1,264 @@
import {
addProjectConfiguration,
joinPathFragments,
ProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
writeJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { exampleRootTslintJson } from '@nrwl/linter';
import { conversionGenerator } from './convert-tslint-to-eslint';
/**
* Don't run actual child_process implementation of installPackagesTask()
*/
jest.mock('child_process');
const appProjectName = 'angular-app-1';
const appProjectRoot = `apps/${appProjectName}`;
const appProjectTSLintJsonPath = joinPathFragments(
appProjectRoot,
'tslint.json'
);
const projectPrefix = 'angular-app';
const libProjectName = 'angular-lib-1';
const libProjectRoot = `libs/${libProjectName}`;
const libProjectTSLintJsonPath = joinPathFragments(
libProjectRoot,
'tslint.json'
);
// Used to configure the test Tree and stub the response from tslint-to-eslint-config util findReportedConfiguration()
const projectTslintJsonData = {
raw: {
extends: '../../tslint.json',
rules: {
// Standard Nx/Angular CLI generated rules
'directive-selector': [true, 'attribute', projectPrefix, 'camelCase'],
'component-selector': [true, 'element', projectPrefix, 'kebab-case'],
// User custom TS rule
'no-empty-interface': true,
// User custom template/HTML rule
'template-banana-in-box': true,
// User custom rule with no known automated converter
'some-super-custom-rule-with-no-converter': true,
},
linterOptions: {
exclude: ['!**/*'],
},
},
tslintPrintConfigResult: {
rules: {
'directive-selector': {
ruleArguments: ['attribute', projectPrefix, 'camelCase'],
ruleSeverity: 'error',
},
'component-selector': {
ruleArguments: ['element', projectPrefix, 'kebab-case'],
ruleSeverity: 'error',
},
'no-empty-interface': {
ruleArguments: [],
ruleSeverity: 'error',
},
'template-banana-in-box': {
ruleArguments: [],
ruleSeverity: 'error',
},
'some-super-custom-rule-with-no-converter': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
},
};
function mockFindReportedConfiguration(_, pathToTslintJson) {
switch (pathToTslintJson) {
case 'tslint.json':
return exampleRootTslintJson.tslintPrintConfigResult;
case appProjectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
case libProjectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
default:
throw new Error(
`mockFindReportedConfiguration - Did not recognize path ${pathToTslintJson}`
);
}
}
/**
* See ./mock-tslint-to-eslint-config.ts for why this is needed
*/
jest.mock('tslint-to-eslint-config', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('tslint-to-eslint-config'),
findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
};
});
/**
* Mock the the mutating fs utilities used within the conversion logic, they are not
* needed because of our stubbed response for findReportedConfiguration() above, and
* they would cause noise in the git data of the actual Nx repo when the tests run.
*/
jest.mock('fs', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('fs'),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
};
});
describe('convert-tslint-to-eslint', () => {
let host: Tree;
beforeEach(async () => {
host = createTreeWithEmptyWorkspace();
writeJson(host, 'tslint.json', exampleRootTslintJson.raw);
addProjectConfiguration(host, appProjectName, {
root: appProjectRoot,
prefix: projectPrefix,
projectType: 'application',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!apps/angular-app-1/**/*'],
tsConfig: ['apps/angular-app-1/tsconfig.app.json'],
},
},
},
} as ProjectConfiguration);
addProjectConfiguration(host, libProjectName, {
root: libProjectRoot,
prefix: projectPrefix,
projectType: 'library',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!libs/angular-lib-1/**/*'],
tsConfig: ['libs/angular-lib-1/tsconfig.app.json'],
},
},
},
} as ProjectConfiguration);
/**
* Existing tslint.json file for the app project
*/
writeJson(
host,
'apps/angular-app-1/tslint.json',
projectTslintJsonData.raw
);
/**
* Existing tslint.json file for the lib project
*/
writeJson(
host,
'libs/angular-lib-1/tslint.json',
projectTslintJsonData.raw
);
});
it('should work for Angular applications', async () => {
await conversionGenerator(host, {
project: appProjectName,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, appProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
expect(readJson(host, '.eslintrc.json')).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(appProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(appProjectTSLintJsonPath)).toEqual(false);
});
it('should work for Angular libraries', async () => {
await conversionGenerator(host, {
project: libProjectName,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, libProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
expect(readJson(host, '.eslintrc.json')).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(libProjectTSLintJsonPath)).toEqual(false);
});
});

View File

@ -0,0 +1,185 @@
import { conversionGenerator as cypressConversionGenerator } from '@nrwl/cypress';
import {
convertNxGenerator,
formatFiles,
GeneratorCallback,
logger,
Tree,
} from '@nrwl/devkit';
import { ConvertTSLintToESLintSchema, ProjectConverter } from '@nrwl/linter';
import type { Linter } from 'eslint';
import { addLintingGenerator } from '../../schematics/add-linting/add-linting';
export async function conversionGenerator(
host: Tree,
options: ConvertTSLintToESLintSchema
) {
/**
* The ProjectConverter instance encapsulates all the standard operations we need
* to perform in order to convert a project from TSLint to ESLint, as well as some
* extensibility points for adjusting the behavior on a per package basis.
*
* E.g. @nrwl/angular projects might need to make different changes to the final
* ESLint config when compared with @nrwl/next projects.
*
* See the ProjectConverter implementation for a full breakdown of what it does.
*/
const projectConverter = new ProjectConverter({
host,
projectName: options.project,
eslintInitializer: async ({ projectName, projectConfig }) => {
await addLintingGenerator(host, {
linter: 'eslint',
projectType: projectConfig.projectType,
projectName,
projectRoot: projectConfig.root,
prefix: (projectConfig as any).prefix || 'app',
});
},
});
/**
* Dynamically install tslint-to-eslint-config to assist with the conversion.
*/
projectConverter.installTSLintToESLintConfigPackage();
/**
* Create the standard (which is applicable to the current package) ESLint setup
* for converting the project.
*/
await projectConverter.initESLint();
/**
* Convert the root tslint.json and apply the converted rules to the root .eslintrc.json
*/
const rootConfigInstallTask = await projectConverter.convertRootTSLintConfig(
(json) => {
json.overrides = [
{ files: ['*.ts'], rules: {} },
{ files: ['*.html'], rules: {} },
];
return applyAngularRulesToCorrectOverrides(json);
}
);
/**
* Convert the project's tslint.json to an equivalent ESLint config.
*/
const projectConfigInstallTask = await projectConverter.convertProjectConfig(
(json) => applyAngularRulesToCorrectOverrides(json)
);
/**
* Clean up the original TSLint configuration for the project.
*/
projectConverter.removeProjectTSLintFile();
/**
* Store user preference regarding removeTSLintIfNoMoreTSLintTargets for the collection
*/
projectConverter.setDefaults(
'@nrwl/angular',
options.removeTSLintIfNoMoreTSLintTargets
);
/**
* If the Angular project is an app which has an e2e project, try and convert that as well.
*/
let cypressInstallTask: GeneratorCallback = () => Promise.resolve(undefined);
const e2eProjectName = projectConverter.getE2EProjectName();
if (e2eProjectName) {
try {
cypressInstallTask = await cypressConversionGenerator(host, {
project: e2eProjectName,
/**
* We can always set this to false, because it will already be handled by the next
* step of this parent generator, if applicable
*/
removeTSLintIfNoMoreTSLintTargets: false,
});
} catch {
logger.warn(
'This Angular app has an e2e project, but it was not possible to convert it from TSLint to ESLint. This could be because the e2e project did not have a tslint.json file to begin with.'
);
}
}
/**
* Based on user preference and remaining usage, remove TSLint from the workspace entirely.
*/
let uninstallTSLintTask: GeneratorCallback = () => Promise.resolve(undefined);
if (
options.removeTSLintIfNoMoreTSLintTargets &&
!projectConverter.isTSLintUsedInWorkspace()
) {
uninstallTSLintTask = projectConverter.removeTSLintFromWorkspace();
}
await formatFiles(host);
return async () => {
projectConverter.uninstallTSLintToESLintConfigPackage();
await rootConfigInstallTask();
await projectConfigInstallTask();
await cypressInstallTask();
await uninstallTSLintTask();
};
}
export const conversionSchematic = convertNxGenerator(conversionGenerator);
/**
* In the case of Angular lint rules, we need to apply them to correct override depending upon whether
* or not they require @typescript-eslint/parser or @angular-eslint/template-parser in order to function.
*
* By this point, the applicable overrides have already been scaffolded for us by the Nx generators
* that ran earlier within this generator.
*/
function applyAngularRulesToCorrectOverrides(
json: Linter.Config
): Linter.Config {
const rules = json.rules;
if (rules && Object.keys(rules).length) {
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
for (const override of json.overrides) {
if (
override.files.includes('*.html') &&
ruleName.startsWith('@angular-eslint/template')
) {
// Prioritize the converted rules over any base implementations from the original Nx generator
override.rules[ruleName] = ruleConfig;
}
/**
* By default, tslint-to-eslint-config will try and apply any rules without known converters
* by using eslint-plugin-tslint. We instead explicitly warn the user about this missing converter,
* and therefore at this point we strip out any rules which start with @typescript-eslint/tslint/config
*/
if (
override.files.includes('*.ts') &&
!ruleName.startsWith('@angular-eslint/template') &&
!ruleName.startsWith('@typescript-eslint/tslint/config')
) {
// Prioritize the converted rules over any base implementations from the original Nx generator
override.rules[ruleName] = ruleConfig;
}
}
}
}
// It's possible that there are plugins to apply to the TS override
if (json.plugins) {
for (const override of json.overrides) {
if (override.files.includes('*.ts')) {
override.plugins = override.plugins || [];
override.plugins = [...override.plugins, ...json.plugins];
}
}
delete json.plugins;
}
/**
* We now no longer need the flat list of rules at the root of the config
* because they have all been applied to an appropriate override.
*/
delete json.rules;
return json;
}

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/schema",
"id": "angular-convert-tslint-to-eslint",
"cli": "nx",
"title": "Convert an Angular project from TSLint to ESLint",
"description": "NOTE: Does not work in --dry-run mode",
"examples": [
{
"command": "g convert-tslint-to-eslint myapp",
"description": "Convert the Angular project `myapp` from TSLint to ESLint"
}
],
"type": "object",
"properties": {
"project": {
"description": "The name of the Angular project to convert.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Which Angular project would you like to convert from TSLint to ESLint?"
},
"removeTSLintIfNoMoreTSLintTargets": {
"type": "boolean",
"description": "If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration",
"default": true,
"x-prompt": "Would you like to remove TSLint and its related config if there are no TSLint projects remaining after this conversion?"
}
},
"required": ["project"]
}

View File

@ -0,0 +1,131 @@
import { join, normalize } from '@angular-devkit/core';
import { chain, noop, Rule, Tree } from '@angular-devkit/schematics';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import {
addLintFiles,
getWorkspacePath,
Linter,
offsetFromRoot,
updateJsonInTree,
} from '@nrwl/workspace';
import {
createAngularEslintJson,
createAngularProjectESLintLintTarget,
extraEslintDependencies,
} from '../../utils/lint';
import { Schema } from './schema';
export default function addLinting(options: Schema): Rule {
return chain([
addLintFiles(options.projectRoot, options.linter, {
onlyGlobal: options.linter === Linter.TsLint, // local lint files are added differently when tslint
localConfig:
options.linter === Linter.TsLint
? undefined
: createAngularEslintJson(options.projectRoot, options.prefix),
extraPackageDeps:
options.linter === Linter.TsLint ? undefined : extraEslintDependencies,
}),
options.projectType === 'application' && options.linter === Linter.TsLint
? updateTsLintConfig(options)
: noop(),
options.projectType === 'library' && options.linter === Linter.TsLint
? updateJsonInTree(`${options.projectRoot}/tslint.json`, (json) => {
return {
...json,
extends: `${offsetFromRoot(options.projectRoot)}tslint.json`,
linterOptions: {
exclude: ['!**/*'],
},
};
})
: noop(),
updateProject(options),
]);
}
function updateTsLintConfig(options: Schema): Rule {
return chain([
updateJsonInTree('tslint.json', (json) => {
if (
json.rulesDirectory &&
json.rulesDirectory.indexOf('node_modules/codelyzer') === -1
) {
json.rulesDirectory.push('node_modules/codelyzer');
json.rules = {
...json.rules,
'directive-selector': [true, 'attribute', 'app', 'camelCase'],
'component-selector': [true, 'element', 'app', 'kebab-case'],
'no-conflicting-lifecycle': true,
'no-host-metadata-property': true,
'no-input-rename': true,
'no-inputs-metadata-property': true,
'no-output-native': true,
'no-output-on-prefix': true,
'no-output-rename': true,
'no-outputs-metadata-property': true,
'template-banana-in-box': true,
'template-no-negated-async': true,
'use-lifecycle-interface': true,
'use-pipe-transform-interface': true,
};
}
return json;
}),
updateJsonInTree(`${options.projectRoot}/tslint.json`, (json) => {
json.extends = `${offsetFromRoot(options.projectRoot)}tslint.json`;
json.linterOptions = {
exclude: ['!**/*'],
};
return json;
}),
]);
}
function updateProject(options: Schema): Rule {
return (host: Tree) => {
return chain([
updateJsonInTree(getWorkspacePath(host), (json) => {
const project = json.projects[options.projectName];
if (options.linter === Linter.TsLint) {
project.architect.lint.options.exclude.push(
'!' + join(normalize(options.projectRoot), '**/*')
);
if (options.projectType === 'application') {
project.architect.lint.options.tsConfig = project.architect.lint.options.tsConfig.filter(
(path) =>
path !==
join(normalize(options.projectRoot), 'tsconfig.spec.json') &&
path !==
join(normalize(options.projectRoot), 'e2e/tsconfig.json')
);
}
if (options.projectType === 'library') {
project.architect.lint.options.tsConfig = Array.from(
new Set(project.architect.lint.options.tsConfig)
);
}
}
if (options.linter === Linter.EsLint) {
project.architect.lint = createAngularProjectESLintLintTarget(
options.projectRoot
);
host.delete(`${options.projectRoot}/tslint.json`);
}
json.projects[options.projectName] = project;
return json;
}),
]);
};
}
export const addLintingGenerator = wrapAngularDevkitSchematic(
'@nrwl/angular',
'add-linting'
);

View File

@ -0,0 +1,9 @@
import { Linter } from '@nrwl/workspace';
export interface Schema {
projectName: string;
projectType: 'application' | 'library';
projectRoot: string;
prefix: string;
linter: Linter;
}

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAngularAddLinting",
"title": "Add linting to an Angular project",
"type": "object",
"properties": {
"prefix": {
"type": "string",
"format": "html-selector",
"description": "The prefix to apply to generated selectors."
},
"projectName": {
"type": "string",
"description": "The name of the selected project."
},
"projectType": {
"type": "string",
"enum": ["application", "library"]
},
"projectRoot": {
"type": "string",
"description": "The path to the root of the selected project."
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["tslint", "eslint"],
"default": "eslint"
}
}
}

View File

@ -24,9 +24,8 @@ import {
replaceNodeValue,
updateJsonInTree,
updateWorkspace,
addLintFiles,
Linter,
generateProjectLint,
Linter,
} from '@nrwl/workspace';
import { join, normalize } from '@angular-devkit/core';
import init from '../init/init';
@ -41,10 +40,6 @@ import {
updateWorkspaceInTree,
appsDir,
} from '@nrwl/workspace/src/utils/ast-utils';
import {
createAngularEslintJson,
extraEslintDependencies,
} from '../../utils/lint';
import { names, offsetFromRoot } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
@ -417,45 +412,6 @@ function updateComponentSpec(options: NormalizedSchema) {
};
}
function updateTsLintConfig(options: NormalizedSchema): Rule {
return chain([
updateJsonInTree('tslint.json', (json) => {
if (
json.rulesDirectory &&
json.rulesDirectory.indexOf('node_modules/codelyzer') === -1
) {
json.rulesDirectory.push('node_modules/codelyzer');
json.rules = {
...json.rules,
'directive-selector': [true, 'attribute', 'app', 'camelCase'],
'component-selector': [true, 'element', 'app', 'kebab-case'],
'no-conflicting-lifecycle': true,
'no-host-metadata-property': true,
'no-input-rename': true,
'no-inputs-metadata-property': true,
'no-output-native': true,
'no-output-on-prefix': true,
'no-output-rename': true,
'no-outputs-metadata-property': true,
'template-banana-in-box': true,
'template-no-negated-async': true,
'use-lifecycle-interface': true,
'use-pipe-transform-interface': true,
};
}
return json;
}),
updateJsonInTree(`${options.appProjectRoot}/tslint.json`, (json) => {
json.extends = `${offsetFromRoot(options.appProjectRoot)}tslint.json`;
json.linterOptions = {
exclude: ['!**/*'],
};
return json;
}),
]);
}
function addSchematicFiles(
appProjectRoot: string,
options: NormalizedSchema
@ -488,30 +444,6 @@ function updateProject(options: NormalizedSchema): Rule {
delete fixedProject.architect.test;
if (options.linter === Linter.TsLint) {
fixedProject.architect.lint.options.tsConfig = fixedProject.architect.lint.options.tsConfig.filter(
(path) =>
path !==
join(normalize(options.appProjectRoot), 'tsconfig.spec.json') &&
path !==
join(normalize(options.appProjectRoot), 'e2e/tsconfig.json')
);
fixedProject.architect.lint.options.exclude.push(
'!' + join(normalize(options.appProjectRoot), '**/*')
);
}
if (options.linter === Linter.EsLint) {
fixedProject.architect.lint.builder = '@nrwl/linter:eslint';
fixedProject.architect.lint.options.lintFilePatterns = [
`${options.appProjectRoot}/src/**/*.ts`,
`${options.appProjectRoot}/src/**/*.html`,
];
delete fixedProject.architect.lint.options.tsConfig;
delete fixedProject.architect.lint.options.exclude;
host.delete(`${options.appProjectRoot}/tslint.json`);
}
if (options.unitTestRunner === 'none') {
host.delete(
`${options.appProjectRoot}/src/app/app.component.spec.ts`
@ -817,18 +749,7 @@ export default function (schema: Schema): Rule {
updateComponentStyles(options),
options.unitTestRunner !== 'none' ? updateComponentSpec(options) : noop(),
options.routing ? addRouterRootConfiguration(options) : noop(),
addLintFiles(options.appProjectRoot, options.linter, {
onlyGlobal: options.linter === Linter.TsLint, // local lint files are added differently when tslint
localConfig:
options.linter === Linter.TsLint
? undefined
: createAngularEslintJson(options.appProjectRoot, options.prefix),
extraPackageDeps:
options.linter === Linter.TsLint
? undefined
: extraEslintDependencies,
}),
options.linter === 'tslint' ? updateTsLintConfig(options) : noop(),
addLinting(options),
options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.name,
@ -860,6 +781,31 @@ export default function (schema: Schema): Rule {
};
}
const addLinting = (options: NormalizedSchema) => () => {
return chain([
schematic('add-linting', {
linter: options.linter,
projectType: 'application',
projectName: options.name,
projectRoot: options.appProjectRoot,
prefix: options.prefix,
}),
/**
* I cannot explain why this extra rule is needed, the add-linting
* schematic applies the exact same host.delete() call but the main
* chain of this schematic still preserves it...
*/
(host) => {
if (
options.linter === Linter.EsLint &&
host.exists(`${options.appProjectRoot}/tslint.json`)
) {
host.delete(`${options.appProjectRoot}/tslint.json`);
}
},
]);
};
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const appDirectory = options.directory
? `${names(options.directory).fileName}/${names(options.name).fileName}`

View File

@ -14,7 +14,6 @@ import {
} from '@angular-devkit/schematics';
import {
getWorkspacePath,
Linter,
replaceAppNameWithPath,
updateJsonInTree,
} from '@nrwl/workspace';
@ -147,28 +146,6 @@ export function updateProject(options: NormalizedSchema): Rule {
delete fixedProject.architect.test;
if (options.linter === Linter.TsLint) {
fixedProject.architect.lint.options.tsConfig = fixedProject.architect.lint.options.tsConfig.filter(
(path) =>
path !==
join(normalize(options.projectRoot), 'tsconfig.spec.json')
);
fixedProject.architect.lint.options.exclude.push(
'!' + join(normalize(options.projectRoot), '**/*')
);
}
if (options.linter === Linter.EsLint) {
fixedProject.architect.lint.builder = '@nrwl/linter:eslint';
fixedProject.architect.lint.options.lintFilePatterns = [
`${options.projectRoot}/src/**/*.ts`,
`${options.projectRoot}/src/**/*.html`,
];
delete fixedProject.architect.lint.options.tsConfig;
delete fixedProject.architect.lint.options.exclude;
host.delete(`${options.projectRoot}/tslint.json`);
}
json.projects[options.name] = fixedProject;
return json;
});
@ -191,17 +168,6 @@ export function updateProject(options: NormalizedSchema): Rule {
},
};
}),
options.linter === Linter.TsLint
? updateJsonInTree(`${options.projectRoot}/tslint.json`, (json) => {
return {
...json,
extends: `${offsetFromRoot(options.projectRoot)}tslint.json`,
linterOptions: {
exclude: ['!**/*'],
},
};
})
: noop(),
updateJsonInTree(`/nx.json`, (json) => {
return {
...json,

View File

@ -1164,6 +1164,7 @@ describe('lib', () => {
{ name: 'myLib', linter: 'eslint' },
appTree
);
expect(tree.exists('libs/my-lib/tslint.json')).toBe(false);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(workspaceJson.projects['my-lib'].architect.lint)
.toMatchInlineSnapshot(`

View File

@ -8,12 +8,7 @@ import {
SchematicsException,
Tree,
} from '@angular-devkit/schematics';
import {
addLintFiles,
formatFiles,
Linter,
updateJsonInTree,
} from '@nrwl/workspace';
import { formatFiles, Linter, updateJsonInTree } from '@nrwl/workspace';
import init, { addUnitTestRunner } from '../init/init';
import { addModule } from './lib/add-module';
import { normalizeOptions } from './lib/normalize-options';
@ -22,11 +17,8 @@ import { updateProject } from './lib/update-project';
import { updateTsConfig } from './lib/update-tsconfig';
import { Schema } from './schema';
import { enableStrictTypeChecking } from './lib/enable-strict-type-checking';
import {
createAngularEslintJson,
extraEslintDependencies,
} from '../../utils/lint';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { NormalizedSchema } from './lib/normalized-schema';
export default function (schema: Schema): Rule {
return (host: Tree): Rule => {
@ -50,17 +42,6 @@ export default function (schema: Schema): Rule {
...options,
skipFormat: true,
}),
addLintFiles(options.projectRoot, options.linter, {
onlyGlobal: options.linter === Linter.TsLint,
localConfig:
options.linter === Linter.TsLint
? undefined
: createAngularEslintJson(options.projectRoot, options.prefix),
extraPackageDeps:
options.linter === Linter.TsLint
? undefined
: extraEslintDependencies,
}),
addUnitTestRunner(options),
// TODO: Remove this after Angular 10.1.0
updateJsonInTree('tsconfig.json', () => ({
@ -101,10 +82,37 @@ export default function (schema: Schema): Rule {
: noop(),
addModule(options),
options.strict ? enableStrictTypeChecking(options) : noop(),
addLinting(options),
formatFiles(options),
]);
};
}
const addLinting = (options: NormalizedSchema) => () => {
return chain([
schematic('add-linting', {
linter: options.linter,
projectType: 'library',
projectName: options.name,
projectRoot: options.projectRoot,
prefix: options.prefix,
}),
/**
* I cannot explain why this extra rule is needed, the add-linting
* schematic applies the exact same host.delete() call but the main
* chain of this library schematic still preserves it...
*/
(host) => {
if (
options.linter === Linter.EsLint &&
host.exists(`${options.projectRoot}/tslint.json`)
) {
host.delete(`${options.projectRoot}/tslint.json`);
}
},
]);
};
export const libraryGenerator = wrapAngularDevkitSchematic(
'@nrwl/angular',
'library'

View File

@ -10,12 +10,12 @@ Array [
"/jest.config.js",
"/jest.preset.js",
"/.eslintrc.json",
"/libs/test-ui-lib/.eslintrc.json",
"/libs/test-ui-lib/README.md",
"/libs/test-ui-lib/tsconfig.lib.json",
"/libs/test-ui-lib/tsconfig.json",
"/libs/test-ui-lib/jest.config.js",
"/libs/test-ui-lib/tsconfig.spec.json",
"/libs/test-ui-lib/.eslintrc.json",
"/libs/test-ui-lib/src/index.ts",
"/libs/test-ui-lib/src/test-setup.ts",
"/libs/test-ui-lib/src/lib/test-ui-lib.module.ts",

View File

@ -1,5 +1,20 @@
import type { TargetDefinition } from '@angular-devkit/core/src/workspace';
import { angularEslintVersion } from './versions';
export function createAngularProjectESLintLintTarget(
projectRoot: string
): TargetDefinition {
return {
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: [
`${projectRoot}/src/**/*.ts`,
`${projectRoot}/src/**/*.html`,
],
},
};
}
export const extraEslintDependencies = {
dependencies: {},
devDependencies: {

View File

@ -1,2 +1,3 @@
export { cypressProjectGenerator } from './src/generators/cypress-project/cypress-project';
export { cypressInitGenerator } from './src/generators/init/init';
export { conversionGenerator } from './src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint';

View File

@ -0,0 +1,282 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convert-tslint-to-eslint should work for Cypress applications 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/eslint-plugin-nx": "*",
"@nrwl/linter": "*",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"eslint": "7.10.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-import": "latest",
},
"name": "test-name",
}
`;
exports[`convert-tslint-to-eslint should work for Cypress applications 2`] = `
Object {
"projectType": "application",
"root": "apps/e2e-app-1",
"targets": Object {
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/e2e-app-1/**/*.{js,ts}",
],
},
},
},
}
`;
exports[`convert-tslint-to-eslint should work for Cypress applications 3`] = `
Object {
"ignorePatterns": Array [
"**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {
"@nrwl/nx/enforce-module-boundaries": Array [
"error",
Object {
"allow": Array [
"@nx-example/shared/product/data/testing",
],
"depConstraints": Array [
Object {
"onlyDependOnLibsWithTags": Array [
"type:feature",
"type:ui",
],
"sourceTag": "type:app",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:ui",
"type:data",
"type:types",
"type:state",
],
"sourceTag": "type:feature",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:types",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:state",
"type:types",
"type:data",
],
"sourceTag": "type:state",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:data",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:e2e-utils",
],
"sourceTag": "type:e2e",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
"type:ui",
],
"sourceTag": "type:ui",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:products",
"scope:shared",
],
"sourceTag": "scope:products",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:cart",
"scope:shared",
],
"sourceTag": "scope:cart",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/typescript",
],
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/javascript",
],
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
],
"plugins": Array [
"eslint-plugin-import",
],
"rules": Object {
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"_buffer",
"_counters",
"_timers",
"_groupDepth",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
],
"plugins": Array [
"@nrwl/nx",
],
"root": true,
}
`;
exports[`convert-tslint-to-eslint should work for Cypress applications 4`] = `
Object {
"extends": Array [
"plugin:cypress/recommended",
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"parserOptions": Object {
"project": "apps/e2e-app-1/tsconfig.*?.json",
},
"rules": Object {},
},
Object {
"files": Array [
"src/plugins/index.js",
],
"rules": Object {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off",
},
},
],
"rules": Object {
"@typescript-eslint/no-empty-interface": "error",
},
}
`;

View File

@ -0,0 +1,161 @@
import {
addProjectConfiguration,
joinPathFragments,
readJson,
readProjectConfiguration,
Tree,
writeJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { exampleRootTslintJson } from '@nrwl/linter';
import { conversionGenerator } from './convert-tslint-to-eslint';
/**
* Don't run actual child_process implementation of installPackagesTask()
*/
jest.mock('child_process');
const projectName = 'e2e-app-1';
const projectRoot = `apps/${projectName}`;
const projectTSLintJsonPath = joinPathFragments(projectRoot, 'tslint.json');
// Used to configure the test Tree and stub the response from tslint-to-eslint-config util findReportedConfiguration()
const projectTslintJsonData = {
raw: {
extends: '../../tslint.json',
rules: {
// User custom TS rule
'no-empty-interface': true,
// User custom rule with no known automated converter
'some-super-custom-rule-with-no-converter': true,
},
linterOptions: {
exclude: ['!**/*'],
},
},
tslintPrintConfigResult: {
rules: {
'no-empty-interface': {
ruleArguments: [],
ruleSeverity: 'error',
},
'some-super-custom-rule-with-no-converter': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
},
};
function mockFindReportedConfiguration(_, pathToTslintJson) {
switch (pathToTslintJson) {
case 'tslint.json':
return exampleRootTslintJson.tslintPrintConfigResult;
case projectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
default:
throw new Error(
`mockFindReportedConfiguration - Did not recognize path ${pathToTslintJson}`
);
}
}
/**
* See ./mock-tslint-to-eslint-config.ts for why this is needed
*/
jest.mock('tslint-to-eslint-config', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('tslint-to-eslint-config'),
findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
};
});
/**
* Mock the the mutating fs utilities used within the conversion logic, they are not
* needed because of our stubbed response for findReportedConfiguration() above, and
* they would cause noise in the git data of the actual Nx repo when the tests run.
*/
jest.mock('fs', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('fs'),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
};
});
describe('convert-tslint-to-eslint', () => {
let host: Tree;
beforeEach(async () => {
host = createTreeWithEmptyWorkspace();
writeJson(host, 'tslint.json', exampleRootTslintJson.raw);
addProjectConfiguration(host, projectName, {
root: projectRoot,
projectType: 'application',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!apps/e2e-app-1/**/*'],
tsConfig: ['apps/e2e-app-1/tsconfig.app.json'],
},
},
},
});
/**
* Existing tslint.json file for the project
*/
writeJson(host, 'apps/e2e-app-1/tslint.json', projectTslintJsonData.raw);
});
it('should work for Cypress applications', async () => {
await conversionGenerator(host, {
project: projectName,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, projectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
expect(readJson(host, '.eslintrc.json')).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(projectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(projectTSLintJsonPath)).toEqual(false);
});
});

View File

@ -0,0 +1,112 @@
import {
convertNxGenerator,
formatFiles,
GeneratorCallback,
Tree,
} from '@nrwl/devkit';
import { ConvertTSLintToESLintSchema, ProjectConverter } from '@nrwl/linter';
import type { Linter } from 'eslint';
import {
addLinter,
CypressProjectSchema,
} from '../cypress-project/cypress-project';
export async function conversionGenerator(
host: Tree,
options: ConvertTSLintToESLintSchema
) {
/**
* The ProjectConverter instance encapsulates all the standard operations we need
* to perform in order to convert a project from TSLint to ESLint, as well as some
* extensibility points for adjusting the behavior on a per package basis.
*
* E.g. @nrwl/angular projects might need to make different changes to the final
* ESLint config when compared with @nrwl/next projects.
*
* See the ProjectConverter implementation for a full breakdown of what it does.
*/
const projectConverter = new ProjectConverter({
host,
projectName: options.project,
eslintInitializer: async ({ projectName, projectConfig }) => {
await addLinter(host, {
linter: 'eslint',
projectName,
projectRoot: projectConfig.root,
} as CypressProjectSchema);
},
});
/**
* Dynamically install tslint-to-eslint-config to assist with the conversion.
*/
projectConverter.installTSLintToESLintConfigPackage();
/**
* Create the standard (which is applicable to the current package) ESLint setup
* for converting the project.
*/
await projectConverter.initESLint();
/**
* Convert the root tslint.json and apply the converted rules to the root .eslintrc.json.
*/
const rootConfigInstallTask = await projectConverter.convertRootTSLintConfig(
(json) => removeCodelyzerRelatedRules(json)
);
/**
* Convert the project's tslint.json to an equivalent ESLint config.
*/
const projectConfigInstallTask = await projectConverter.convertProjectConfig(
(json) => json
);
/**
* Clean up the original TSLint configuration for the project.
*/
projectConverter.removeProjectTSLintFile();
/**
* Store user preference regarding removeTSLintIfNoMoreTSLintTargets for the collection
*/
projectConverter.setDefaults(
'@nrwl/cypress',
options.removeTSLintIfNoMoreTSLintTargets
);
/**
* Based on user preference and remaining usage, remove TSLint from the workspace entirely.
*/
let uninstallTSLintTask: GeneratorCallback = () => Promise.resolve(undefined);
if (
options.removeTSLintIfNoMoreTSLintTargets &&
!projectConverter.isTSLintUsedInWorkspace()
) {
uninstallTSLintTask = projectConverter.removeTSLintFromWorkspace();
}
await formatFiles(host);
return async () => {
projectConverter.uninstallTSLintToESLintConfigPackage();
await rootConfigInstallTask();
await projectConfigInstallTask();
await uninstallTSLintTask();
};
}
export const conversionSchematic = convertNxGenerator(conversionGenerator);
/**
* Remove any @angular-eslint rules that were applied as a result of converting prior codelyzer
* rules, because they are only relevant for Angular projects.
*/
function removeCodelyzerRelatedRules(json: Linter.Config): Linter.Config {
for (const ruleName of Object.keys(json.rules)) {
if (ruleName.startsWith('@angular-eslint')) {
delete json.rules[ruleName];
}
}
return json;
}

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/schema",
"id": "cypress-convert-tslint-to-eslint",
"cli": "nx",
"title": "Convert a Cypress project from TSLint to ESLint",
"description": "NOTE: Does not work in --dry-run mode",
"examples": [
{
"command": "g convert-tslint-to-eslint myapp",
"description": "Convert the Cypress project `myapp` from TSLint to ESLint"
}
],
"type": "object",
"properties": {
"project": {
"description": "The name of the Cypress project to convert.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Which Cypress project would you like to convert from TSLint to ESLint?"
},
"removeTSLintIfNoMoreTSLintTargets": {
"type": "boolean",
"description": "If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration",
"default": true,
"x-prompt": "Would you like to remove TSLint and its related config if there are no TSLint projects remaining after this conversion?"
}
},
"required": ["project"]
}

View File

@ -64,7 +64,7 @@ function addProject(host: Tree, options: CypressProjectSchema) {
});
}
async function addLinter(host: Tree, options: CypressProjectSchema) {
export async function addLinter(host: Tree, options: CypressProjectSchema) {
const installTask = await lintProjectGenerator(host, {
project: options.projectName,
linter: options.linter,

View File

@ -37,7 +37,10 @@ export { parseTargetString } from './src/executors/parse-target-string';
export { readTargetOptions } from './src/executors/read-target-options';
export { readJson, writeJson, updateJson } from './src/utils/json';
export { addDependenciesToPackageJson } from './src/utils/package-json';
export {
addDependenciesToPackageJson,
removeDependenciesFromPackageJson,
} from './src/utils/package-json';
export { installPackagesTask } from './src/tasks/install-packages-task';
export { names } from './src/utils/names';
export {

View File

@ -47,6 +47,49 @@ export function addDependenciesToPackageJson(
};
}
/**
* Remove Dependencies and Dev Dependencies from package.json
*
* For example, `removeDependenciesFromPackageJson(host, ['react'], ['jest'])`
* will remove `react` and `jest` from the dependencies and devDependencies sections of package.json respectively
*
* @param dependencies Dependencies to be removed from the dependencies section of package.json
* @param devDependencies Dependencies to be removed from the devDependencies section of package.json
* @returns Callback to uninstall dependencies only if necessary. undefined is returned if changes are not necessary.
*/
export function removeDependenciesFromPackageJson(
host: Tree,
dependencies: string[],
devDependencies: string[],
packageJsonPath: string = 'package.json'
): GeneratorCallback {
const currentPackageJson = readJson(host, packageJsonPath);
if (
requiresRemovingOfPackages(
currentPackageJson,
dependencies,
devDependencies
)
) {
updateJson(host, packageJsonPath, (json) => {
for (const dep of dependencies) {
delete json.dependencies[dep];
}
for (const devDep of devDependencies) {
delete json.devDependencies[devDep];
}
json.dependencies = sortObjectByKeys(json.dependencies);
json.devDependencies = sortObjectByKeys(json.devDependencies);
return json;
});
}
return () => {
installPackagesTask(host);
};
}
function sortObjectByKeys(obj: unknown) {
return Object.keys(obj)
.sort()
@ -83,3 +126,31 @@ function requiresAddingOfPackages(packageJsonFile, deps, devDeps): boolean {
return needsDepsUpdate || needsDevDepsUpdate;
}
/**
* Verifies whether the given packageJson dependencies require an update
* given the deps & devDeps passed in
*/
function requiresRemovingOfPackages(
packageJsonFile,
deps: string[],
devDeps: string[]
): boolean {
let needsDepsUpdate = false;
let needsDevDepsUpdate = false;
packageJsonFile.dependencies = packageJsonFile.dependencies || {};
packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
if (deps.length > 0) {
needsDepsUpdate = deps.some((entry) => packageJsonFile.dependencies[entry]);
}
if (devDeps.length > 0) {
needsDevDepsUpdate = devDeps.some(
(entry) => packageJsonFile.devDependencies[entry]
);
}
return needsDepsUpdate || needsDevDepsUpdate;
}

View File

@ -10,6 +10,11 @@
* package.
*/
export default {
env: {
browser: true,
es6: true,
node: true,
},
plugins: ['@angular-eslint'],
extends: ['plugin:@angular-eslint/recommended'],
rules: {},

View File

@ -1,3 +1,4 @@
export { lintProjectGenerator } from './src/generators/lint-project/lint-project';
export { lintInitGenerator } from './src/generators/init/init';
export { Linter } from './src/generators/utils/linter';
export * from './src/utils/convert-tslint-to-eslint';

View File

@ -0,0 +1,224 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convertToESLintConfig() should work for a non-Angular project tslint.json file 1`] = `
Object {
"convertedESLintConfig": Object {
"env": Object {
"browser": true,
"es6": true,
"node": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": Object {
"project": "tsconfig.json",
"sourceType": "module",
},
"plugins": Array [
"@typescript-eslint",
],
},
"ensureESLintPlugins": Array [],
"unconvertedTSLintRules": Array [],
}
`;
exports[`convertToESLintConfig() should work for a project tslint.json file 1`] = `
Object {
"convertedESLintConfig": Object {
"env": Object {
"browser": true,
"es6": true,
"node": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": Object {
"project": "tsconfig.json",
"sourceType": "module",
},
"plugins": Array [
"@angular-eslint/eslint-plugin",
"@typescript-eslint",
],
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "angular-app",
"style": "camelCase",
"type": "attribute",
},
],
},
},
"ensureESLintPlugins": Array [],
"unconvertedTSLintRules": Array [],
}
`;
exports[`convertToESLintConfig() should work for a root tslint.json file 1`] = `
Object {
"convertedESLintConfig": Object {
"env": Object {
"browser": true,
"es6": true,
"node": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": Object {
"project": "tsconfig.json",
"sourceType": "module",
},
"plugins": Array [
"eslint-plugin-import",
"@angular-eslint/eslint-plugin",
"@angular-eslint/eslint-plugin-template",
"@typescript-eslint",
],
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "app",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "app",
"style": "camelCase",
"type": "attribute",
},
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/template/banana-in-box": "error",
"@angular-eslint/template/no-negated-async": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"_buffer",
"_counters",
"_groupDepth",
"_timers",
"assert",
"clear",
"count",
"countReset",
"dir",
"dirxml",
"error",
"group",
"groupCollapsed",
"groupEnd",
"log",
"table",
"timeLog",
"warn",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
"ensureESLintPlugins": Array [
"eslint-plugin-import",
],
"unconvertedTSLintRules": Array [],
}
`;
exports[`convertToESLintConfig() should work for an e2e project tslint.json file 1`] = `
Object {
"convertedESLintConfig": Object {
"env": Object {
"browser": true,
"es6": true,
"node": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": Object {
"project": "tsconfig.json",
"sourceType": "module",
},
"plugins": Array [
"@typescript-eslint",
],
},
"ensureESLintPlugins": Array [],
"unconvertedTSLintRules": Array [],
}
`;

View File

@ -0,0 +1,336 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProjectConverter removeTSLintFromWorkspace() should remove all relevant traces of TSLint from the workspace 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"codelyzer": "latest",
"tslint": "latest",
},
"name": "test-name",
}
`;
exports[`ProjectConverter removeTSLintFromWorkspace() should remove all relevant traces of TSLint from the workspace 2`] = `
Object {
"generators": Object {
"@nrwl/angular": Object {
"application": Object {
"linter": "tslint",
},
"library": Object {
"linter": "tslint",
},
},
},
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
},
"@nrwl/angular:library": Object {
"linter": "tslint",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter removeTSLintFromWorkspace() should remove all relevant traces of TSLint from the workspace 3`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {},
"name": "test-name",
}
`;
exports[`ProjectConverter removeTSLintFromWorkspace() should remove all relevant traces of TSLint from the workspace 4`] = `
Object {
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"unitTestRunner": "jest",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter removeTSLintFromWorkspace() should remove the entry in generators for convert-tslint-to-eslint because it is no longer needed 1`] = `
Object {
"generators": Object {
"@nrwl/angular": Object {
"convert-tslint-to-eslint": Object {
"removeTSLintIfNoMoreTSLintTargets": true,
},
},
},
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
},
"@nrwl/angular:library": Object {
"linter": "tslint",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter removeTSLintFromWorkspace() should remove the entry in generators for convert-tslint-to-eslint because it is no longer needed 2`] = `
Object {
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"unitTestRunner": "jest",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter setDefaults() should set the default configuration for removeTSLintIfNoMoreTSLintTargets in workspace.json 1`] = `
Object {
"generators": Object {
"@nrwl/angular": Object {
"application": Object {
"linter": "tslint",
},
"library": Object {
"linter": "tslint",
},
},
},
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
},
"@nrwl/angular:library": Object {
"linter": "tslint",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter setDefaults() should set the default configuration for removeTSLintIfNoMoreTSLintTargets in workspace.json 2`] = `
Object {
"generators": Object {
"@nrwl/angular": Object {
"application": Object {
"linter": "tslint",
},
"convert-tslint-to-eslint": Object {
"removeTSLintIfNoMoreTSLintTargets": true,
},
"library": Object {
"linter": "tslint",
},
},
},
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
},
"@nrwl/angular:library": Object {
"linter": "tslint",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter setDefaults() should set the default configuration for removeTSLintIfNoMoreTSLintTargets in workspace.json 3`] = `
Object {
"generators": Object {
"@nrwl/angular": Object {
"application": Object {
"linter": "tslint",
},
"convert-tslint-to-eslint": Object {
"removeTSLintIfNoMoreTSLintTargets": false,
},
"library": Object {
"linter": "tslint",
},
},
},
"projects": Object {
"foo": Object {
"generators": Object {
"@nrwl/angular:application": Object {
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
},
"@nrwl/angular:library": Object {
"linter": "tslint",
},
},
"projectType": "application",
"root": "apps/foo",
"targets": Object {
"lint": Object {
"executor": "@angular-devkit/build-angular:tslint",
"options": Object {
"exclude": Array [
"**/node_modules/**",
"!apps/foo/**/*",
],
"tsConfig": Array [
"apps/foo/tsconfig.app.json",
],
},
},
},
},
},
"version": 1,
}
`;
exports[`ProjectConverter should throw if --dry-run is set 1`] = `
"NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the \\"Run\\" button.
"
`;
exports[`ProjectConverter should throw if --dryRun is set 1`] = `
"NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the \\"Run\\" button.
"
`;
exports[`ProjectConverter should throw if -d is set 1`] = `
"NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the \\"Run\\" button.
"
`;
exports[`ProjectConverter should throw if no project tslint.json is found 1`] = `"We could not find a tslint.json for the selected project \\"apps/foo/tslint.json\\", maybe you have already migrated to ESLint?"`;
exports[`ProjectConverter should throw if no root tslint.json is found 1`] = `"We could not find a tslint.json at the root of your workspace, maybe you have already migrated to ESLint?"`;

View File

@ -0,0 +1,166 @@
import { convertTslintNxRuleToEslintNxRule } from './convert-nx-enforce-module-boundaries-rule';
describe('convertTslintNxRuleToEslintNxRule()', () => {
const configFromNxExamplesRepo = {
allow: ['@nx-example/shared/product/data/testing'],
depConstraints: [
{
sourceTag: 'type:app',
onlyDependOnLibsWithTags: ['type:feature', 'type:ui'],
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:ui',
'type:data',
'type:types',
'type:state',
],
},
{
sourceTag: 'type:types',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:state',
onlyDependOnLibsWithTags: ['type:state', 'type:types', 'type:data'],
},
{
sourceTag: 'type:data',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:e2e',
onlyDependOnLibsWithTags: ['type:e2e-utils'],
},
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: ['type:types', 'type:ui'],
},
{
sourceTag: 'scope:products',
onlyDependOnLibsWithTags: ['scope:products', 'scope:shared'],
},
{
sourceTag: 'scope:cart',
onlyDependOnLibsWithTags: ['scope:cart', 'scope:shared'],
},
],
enforceBuildableLibDependency: true,
};
const testCases = [
{
tslintJson: {},
// Should return null if no existing config found
expected: null,
},
{
// Real usage in nx-examples repo
tslintJson: {
rules: {
'nx-enforce-module-boundaries': [true, configFromNxExamplesRepo],
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['error', configFromNxExamplesRepo],
},
},
{
// Should respect boolean
tslintJson: {
defaultSeverity: 'warning',
rules: {
'nx-enforce-module-boundaries': [false, configFromNxExamplesRepo],
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['off', configFromNxExamplesRepo],
},
},
{
// Should respect boolean + defaultSeverity format
tslintJson: {
defaultSeverity: 'warning',
rules: {
'nx-enforce-module-boundaries': [true, configFromNxExamplesRepo],
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['warn', configFromNxExamplesRepo],
},
},
{
// Should respect object format
tslintJson: {
rules: {
'nx-enforce-module-boundaries': {
severity: 'error',
options: [configFromNxExamplesRepo],
},
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['error', configFromNxExamplesRepo],
},
},
{
// Should respect object format
tslintJson: {
rules: {
'nx-enforce-module-boundaries': {
severity: 'warning',
options: [configFromNxExamplesRepo],
},
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['warn', configFromNxExamplesRepo],
},
},
{
// Should respect object format
tslintJson: {
rules: {
'nx-enforce-module-boundaries': {
severity: 'off',
options: [configFromNxExamplesRepo],
},
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['off', configFromNxExamplesRepo],
},
},
{
// Should respect object format + defaultSeverity option
tslintJson: {
defaultSeverity: 'warning',
rules: {
'nx-enforce-module-boundaries': {
severity: 'default',
options: [configFromNxExamplesRepo],
},
},
},
expected: {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: ['warn', configFromNxExamplesRepo],
},
},
];
testCases.forEach((tc, i) => {
it(`should appropriately convert the nx-enforce-module-boundaries rule usage from TSLint, CASE ${i}`, () => {
expect(convertTslintNxRuleToEslintNxRule(tc.tslintJson)).toEqual(
tc.expected
);
});
});
});

View File

@ -0,0 +1,78 @@
import type { ESLintRuleSeverity } from 'tslint-to-eslint-config';
type TSLintRuleSeverity = 'default' | 'warning' | 'error' | 'off' | boolean;
type TSLintRuleSeverityNonDefaultString = Exclude<
TSLintRuleSeverity,
boolean | 'default'
>;
function convertTSLintRuleSeverity(
tslintConfig: any,
tslintSeverity: TSLintRuleSeverity
): ESLintRuleSeverity {
if (tslintSeverity === true) {
tslintSeverity = 'default';
}
if (tslintSeverity === false) {
tslintSeverity = 'off';
}
if (tslintSeverity === 'default') {
tslintSeverity = tslintConfig.defaultSeverity || 'error';
}
const narrowedTslintSeverity = tslintSeverity as TSLintRuleSeverityNonDefaultString;
return narrowedTslintSeverity === 'warning' ? 'warn' : narrowedTslintSeverity;
}
const NX_TSLINT_RULE_NAME = 'nx-enforce-module-boundaries';
export function convertTslintNxRuleToEslintNxRule(
tslintJson: Record<string, unknown>
): {
ruleName: string;
ruleConfig: [ESLintRuleSeverity, Record<string, unknown>];
} | null {
/**
* TSLint supports a number of different formats for rule configuration
*/
const existingRuleDefinition = tslintJson?.rules?.[NX_TSLINT_RULE_NAME];
if (!existingRuleDefinition) {
return null;
}
let existingRuleSeverity: TSLintRuleSeverity = 'error';
let existingRuleConfig = {
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
};
if (Array.isArray(existingRuleDefinition)) {
existingRuleSeverity = existingRuleDefinition[0];
existingRuleConfig = existingRuleDefinition[1];
} else if (
typeof existingRuleDefinition === 'object' &&
existingRuleDefinition.severity
) {
existingRuleSeverity = existingRuleDefinition.severity;
if (
Array.isArray(existingRuleDefinition.options) &&
existingRuleDefinition.options[0]
) {
existingRuleConfig = existingRuleDefinition.options[0];
}
}
const ruleSeverity: ESLintRuleSeverity = convertTSLintRuleSeverity(
tslintJson,
existingRuleSeverity
);
return {
ruleName: '@nrwl/nx/enforce-module-boundaries',
ruleConfig: [ruleSeverity, existingRuleConfig],
};
}

View File

@ -0,0 +1,102 @@
import {
exampleE2eProjectTslintJson,
exampleAngularProjectTslintJson,
exampleRootTslintJson,
exampleNonAngularProjectTslintJson,
} from './example-tslint-configs';
import { convertToESLintConfig } from './convert-to-eslint-config';
/**
* The actual `findReportedConfiguration()` function is used to execute
* `tslint --print-config` in a child process and read from the real
* file system. This won't work for us in tests where we are dealing
* with a Tree, so we mock out the responses from `findReportedConfiguration()`
* with previously captured result data from that same command.
*/
export function mockFindReportedConfiguration(_, pathToTSLintJson) {
if (
pathToTSLintJson === 'tslint.json' ||
pathToTSLintJson === '/tslint.json'
) {
return exampleRootTslintJson.tslintPrintConfigResult;
}
if (
pathToTSLintJson === 'apps/app1/tslint.json' ||
pathToTSLintJson === 'libs/lib1/tslint.json'
) {
return exampleAngularProjectTslintJson.tslintPrintConfigResult;
}
if (pathToTSLintJson === 'apps/app1-e2e/tslint.json') {
return exampleE2eProjectTslintJson.tslintPrintConfigResult;
}
if (pathToTSLintJson === 'apps/app2/tslint.json') {
return exampleNonAngularProjectTslintJson.tslintPrintConfigResult;
}
throw new Error(
`${pathToTSLintJson} is not a part of the supported mock data for these tests`
);
}
/**
* See ./mock-tslint-to-eslint-config.ts for why this is needed
*/
jest.mock('tslint-to-eslint-config', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('tslint-to-eslint-config'),
findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
};
});
describe('convertToESLintConfig()', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should work for a root tslint.json file', async () => {
const converted = await convertToESLintConfig(
'tslint.json',
exampleRootTslintJson.raw,
[]
);
// Ensure no-console snapshot is deterministic
converted.convertedESLintConfig.rules['no-console'][1].allow.sort();
expect(converted).toMatchSnapshot();
});
it('should work for a project tslint.json file', async () => {
await expect(
convertToESLintConfig(
'apps/app1/tslint.json',
exampleAngularProjectTslintJson.raw,
[]
)
).resolves.toMatchSnapshot();
});
it('should work for an e2e project tslint.json file', async () => {
await expect(
convertToESLintConfig(
'apps/app1-e2e/tslint.json',
exampleE2eProjectTslintJson.raw,
[]
)
).resolves.toMatchSnapshot();
});
it('should work for a non-Angular project tslint.json file', async () => {
await expect(
convertToESLintConfig(
'apps/app2/tslint.json',
exampleNonAngularProjectTslintJson.raw,
[]
)
).resolves.toMatchSnapshot();
});
});

View File

@ -0,0 +1,151 @@
import type { Linter as ESLintLinter } from 'eslint';
import { mkdirSync, writeFileSync } from 'fs';
import { dirname } from 'path';
import type {
TSLintRuleOptions,
createESLintConfiguration as CreateESLintConfiguration,
} from 'tslint-to-eslint-config';
export async function convertToESLintConfig(
pathToTslintJson: string,
tslintJson: Record<string, unknown>,
ignoreExtendsVals: string[]
): Promise<{
convertedESLintConfig: ESLintLinter.Config;
unconvertedTSLintRules: TSLintRuleOptions[];
ensureESLintPlugins: string[];
}> {
/**
* We need to avoid a direct dependency on tslint-to-eslint-config
* and ensure we are only resolving the dependency from the user's
* node_modules on demand (it will be installed as part of the
* conversion generator).
*/
const {
createESLintConfiguration,
findReportedConfiguration,
joinConfigConversionResults,
} = require('tslint-to-eslint-config');
const updatedTSLintJson = tslintJson;
/**
* If ignoreExtendsVals are provided, strip them from the config
* and commit the result to disk per the notes below.
*/
if (ignoreExtendsVals.length && updatedTSLintJson.extends) {
if (
typeof updatedTSLintJson.extends === 'string' &&
ignoreExtendsVals.includes(updatedTSLintJson.extends)
) {
delete updatedTSLintJson.extends;
}
if (Array.isArray(updatedTSLintJson.extends)) {
updatedTSLintJson.extends = updatedTSLintJson.extends.filter(
(ext) => !ignoreExtendsVals.includes(ext)
);
}
/**
* The reasons we need to interact with the filesystem here:
*
* 1) The result of the tslint CLI flag `--print-config` is needed for the
* conversion process, and unfortunately no equivalent Node API was ever
* added to tslint, so the tslint CLI needs to always read from disk.
*
* 2) When converting project configs, we need to strip the extends path
* which corresponds to the workspace's root config, otherwise all of the
* root config's rules will be included in the resultant eslint config for
* the project. The interaction with the filesystem is needed because of
* point (1) above - we need to strip the relevant extends and commit that
* change to disk before the tslint CLI reads the config file.
*/
mkdirSync(dirname(pathToTslintJson), { recursive: true });
writeFileSync(pathToTslintJson, JSON.stringify(updatedTSLintJson));
}
const reportedConfiguration = await findReportedConfiguration(
'npx tslint --print-config',
pathToTslintJson
);
if (reportedConfiguration instanceof Error) {
if (
reportedConfiguration.message.includes('unknown option `--print-config')
) {
throw new Error(
'\nError: TSLint v5.18 required in order to run this schematic. Please update your version and try again.\n'
);
}
/**
* Make a print-config issue easier to understand for the end user.
* This error could occur if, for example, the user does not have a TSLint plugin installed correctly that they
* reference in their config.
*/
const printConfigFailureMessageStart =
'Command failed: npx tslint --print-config "tslint.json"';
if (
reportedConfiguration.message.startsWith(printConfigFailureMessageStart)
) {
throw new Error(
`\nThere was a critical error when trying to inspect your tslint.json: \n${reportedConfiguration.message.replace(
printConfigFailureMessageStart,
''
)}`
);
}
throw new Error(`Unexpected error: ${reportedConfiguration.message}`);
}
const originalConfigurations = {
tslint: {
full: reportedConfiguration,
raw: updatedTSLintJson,
},
};
const summarizedConfiguration = await (createESLintConfiguration as typeof CreateESLintConfiguration)(
originalConfigurations
);
/**
* We are expecting it to not find a converter for nx-enforce-module-boundaries
* and we will explicitly replace it with the ESLint equivalent ourselves.
*/
if (summarizedConfiguration.missing) {
summarizedConfiguration.missing = summarizedConfiguration.missing.filter(
(missingRuleData) =>
missingRuleData.ruleName !== 'nx-enforce-module-boundaries'
);
}
// These are already covered by our extraEslintDependencies which get installed by the schematic
const expectedESLintPlugins = [
'@angular-eslint/eslint-plugin',
'@angular-eslint/eslint-plugin-template',
];
const convertedESLintConfig = joinConfigConversionResults(
summarizedConfiguration,
originalConfigurations
) as ESLintLinter.Config;
if (
Array.isArray(convertedESLintConfig.extends) &&
convertedESLintConfig.extends.length
) {
// Ignore any tslint-to-eslint-config default extends that do not apply to Nx
convertedESLintConfig.extends = convertedESLintConfig.extends.filter(
(ext) => !ext.startsWith('prettier')
);
if (convertedESLintConfig.extends.length === 0) {
delete convertedESLintConfig.extends;
}
}
return {
convertedESLintConfig,
unconvertedTSLintRules: summarizedConfiguration.missing,
ensureESLintPlugins: Array.from(summarizedConfiguration.plugins).filter(
(pluginName) => !expectedESLintPlugins.includes(pluginName)
),
};
}

View File

@ -0,0 +1,324 @@
// Based on latest Angular project root tslint.json + enforce-module-boundaries config from nx-examples
export const exampleRootTslintJson = {
raw: {
rulesDirectory: [
'node_modules/@nrwl/workspace/src/tslint',
'node_modules/codelyzer',
],
linterOptions: {
exclude: ['**/*'],
},
rules: {
'arrow-return-shorthand': true,
'callable-types': true,
'class-name': true,
deprecation: {
severity: 'warn',
},
forin: true,
'import-blacklist': [true, 'rxjs/Rx'],
'interface-over-type-literal': true,
'member-access': false,
'member-ordering': [
true,
{
order: [
'static-field',
'instance-field',
'static-method',
'instance-method',
],
},
],
'no-arg': true,
'no-bitwise': true,
'no-console': [true, 'debug', 'info', 'time', 'timeEnd', 'trace'],
'no-construct': true,
'no-debugger': true,
'no-duplicate-super': true,
'no-empty': false,
'no-empty-interface': true,
'no-eval': true,
'no-inferrable-types': [true, 'ignore-params'],
'no-misused-new': true,
'no-non-null-assertion': true,
'no-shadowed-variable': true,
'no-string-literal': false,
'no-string-throw': true,
'no-switch-case-fall-through': true,
'no-unnecessary-initializer': true,
'no-unused-expression': true,
'no-var-keyword': true,
'object-literal-sort-keys': false,
'prefer-const': true,
radix: true,
'triple-equals': [true, 'allow-null-check'],
'unified-signatures': true,
'variable-name': false,
'nx-enforce-module-boundaries': [
true,
{
allow: ['@nx-example/shared/product/data/testing'],
depConstraints: [
{
sourceTag: 'type:app',
onlyDependOnLibsWithTags: ['type:feature', 'type:ui'],
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:ui',
'type:data',
'type:types',
'type:state',
],
},
{
sourceTag: 'type:types',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:state',
onlyDependOnLibsWithTags: [
'type:state',
'type:types',
'type:data',
],
},
{
sourceTag: 'type:data',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:e2e',
onlyDependOnLibsWithTags: ['type:e2e-utils'],
},
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: ['type:types', 'type:ui'],
},
{
sourceTag: 'scope:products',
onlyDependOnLibsWithTags: ['scope:products', 'scope:shared'],
},
{
sourceTag: 'scope:cart',
onlyDependOnLibsWithTags: ['scope:cart', 'scope:shared'],
},
],
enforceBuildableLibDependency: true,
},
],
'directive-selector': [true, 'attribute', 'app', 'camelCase'],
'component-selector': [true, 'element', 'app', 'kebab-case'],
'no-conflicting-lifecycle': true,
'no-host-metadata-property': true,
'no-input-rename': true,
'no-inputs-metadata-property': true,
'no-output-native': true,
'no-output-on-prefix': true,
'no-output-rename': true,
'no-outputs-metadata-property': true,
'template-banana-in-box': true,
'template-no-negated-async': true,
'use-lifecycle-interface': true,
'use-pipe-transform-interface': true,
},
},
tslintPrintConfigResult: {
rules: {
'arrow-return-shorthand': { ruleArguments: [], ruleSeverity: 'error' },
'callable-types': { ruleArguments: [], ruleSeverity: 'error' },
'class-name': { ruleArguments: [], ruleSeverity: 'error' },
deprecation: { ruleSeverity: 'warning' },
forin: { ruleArguments: [], ruleSeverity: 'error' },
'import-blacklist': { ruleArguments: ['rxjs/Rx'], ruleSeverity: 'error' },
'interface-over-type-literal': {
ruleArguments: [],
ruleSeverity: 'error',
},
'member-access': { ruleArguments: [], ruleSeverity: 'off' },
'member-ordering': {
ruleArguments: [
{
order: [
'static-field',
'instance-field',
'static-method',
'instance-method',
],
},
],
ruleSeverity: 'error',
},
'no-arg': { ruleArguments: [], ruleSeverity: 'error' },
'no-bitwise': { ruleArguments: [], ruleSeverity: 'error' },
'no-console': {
ruleArguments: ['debug', 'info', 'time', 'timeEnd', 'trace'],
ruleSeverity: 'error',
},
'no-construct': { ruleArguments: [], ruleSeverity: 'error' },
'no-debugger': { ruleArguments: [], ruleSeverity: 'error' },
'no-duplicate-super': { ruleArguments: [], ruleSeverity: 'error' },
'no-empty': { ruleArguments: [], ruleSeverity: 'off' },
'no-empty-interface': { ruleArguments: [], ruleSeverity: 'error' },
'no-eval': { ruleArguments: [], ruleSeverity: 'error' },
'no-inferrable-types': {
ruleArguments: ['ignore-params'],
ruleSeverity: 'error',
},
'no-misused-new': { ruleArguments: [], ruleSeverity: 'error' },
'no-non-null-assertion': { ruleArguments: [], ruleSeverity: 'error' },
'no-shadowed-variable': { ruleArguments: [], ruleSeverity: 'error' },
'no-string-literal': { ruleArguments: [], ruleSeverity: 'off' },
'no-string-throw': { ruleArguments: [], ruleSeverity: 'error' },
'no-switch-case-fall-through': {
ruleArguments: [],
ruleSeverity: 'error',
},
'no-unnecessary-initializer': {
ruleArguments: [],
ruleSeverity: 'error',
},
'no-unused-expression': { ruleArguments: [], ruleSeverity: 'error' },
'no-var-keyword': { ruleArguments: [], ruleSeverity: 'error' },
'object-literal-sort-keys': { ruleArguments: [], ruleSeverity: 'off' },
'prefer-const': { ruleArguments: [], ruleSeverity: 'error' },
radix: { ruleArguments: [], ruleSeverity: 'error' },
'triple-equals': {
ruleArguments: ['allow-null-check'],
ruleSeverity: 'error',
},
'unified-signatures': { ruleArguments: [], ruleSeverity: 'error' },
'variable-name': { ruleArguments: [], ruleSeverity: 'off' },
'nx-enforce-module-boundaries': {
ruleArguments: [
{
allow: ['@nx-example/shared/product/data/testing'],
depConstraints: [
{
sourceTag: 'type:app',
onlyDependOnLibsWithTags: ['type:feature', 'type:ui'],
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:ui',
'type:data',
'type:types',
'type:state',
],
},
{
sourceTag: 'type:types',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:state',
onlyDependOnLibsWithTags: [
'type:state',
'type:types',
'type:data',
],
},
{
sourceTag: 'type:data',
onlyDependOnLibsWithTags: ['type:types'],
},
{
sourceTag: 'type:e2e',
onlyDependOnLibsWithTags: ['type:e2e-utils'],
},
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: ['type:types', 'type:ui'],
},
{
sourceTag: 'scope:products',
onlyDependOnLibsWithTags: ['scope:products', 'scope:shared'],
},
{
sourceTag: 'scope:cart',
onlyDependOnLibsWithTags: ['scope:cart', 'scope:shared'],
},
],
enforceBuildableLibDependency: true,
},
],
ruleSeverity: 'error',
},
'directive-selector': {
ruleArguments: ['attribute', 'app', 'camelCase'],
ruleSeverity: 'error',
},
'component-selector': {
ruleArguments: ['element', 'app', 'kebab-case'],
ruleSeverity: 'error',
},
'no-conflicting-lifecycle': { ruleArguments: [], ruleSeverity: 'error' },
'no-host-metadata-property': { ruleArguments: [], ruleSeverity: 'error' },
'no-input-rename': { ruleArguments: [], ruleSeverity: 'error' },
'no-inputs-metadata-property': {
ruleArguments: [],
ruleSeverity: 'error',
},
'no-output-native': { ruleArguments: [], ruleSeverity: 'error' },
'no-output-on-prefix': { ruleArguments: [], ruleSeverity: 'error' },
'no-output-rename': { ruleArguments: [], ruleSeverity: 'error' },
'no-outputs-metadata-property': {
ruleArguments: [],
ruleSeverity: 'error',
},
'template-banana-in-box': { ruleArguments: [], ruleSeverity: 'error' },
'template-no-negated-async': { ruleArguments: [], ruleSeverity: 'error' },
'use-lifecycle-interface': { ruleArguments: [], ruleSeverity: 'error' },
'use-pipe-transform-interface': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
},
};
export const exampleAngularProjectTslintJson = {
raw: {
extends: '../../tslint.json',
rules: {
'directive-selector': [true, 'attribute', 'angular-app', 'camelCase'],
'component-selector': [true, 'element', 'angular-app', 'kebab-case'],
},
linterOptions: {
exclude: ['!**/*'],
},
},
tslintPrintConfigResult: {
rules: {
'directive-selector': {
ruleArguments: ['attribute', 'angular-app', 'camelCase'],
ruleSeverity: 'error',
},
'component-selector': {
ruleArguments: ['element', 'angular-app', 'kebab-case'],
ruleSeverity: 'error',
},
},
},
};
export const exampleNonAngularProjectTslintJson = {
raw: {
extends: '../../tslint.json',
linterOptions: { exclude: ['!**/*'] },
rules: {},
},
tslintPrintConfigResult: { rules: {} },
};
export const exampleE2eProjectTslintJson = {
raw: {
extends: '../../tslint.json',
linterOptions: { exclude: ['!**/*'] },
rules: {},
},
tslintPrintConfigResult: { rules: {} },
};

View File

@ -0,0 +1,7 @@
export {
ConvertTSLintToESLintSchema,
ProjectConverter,
} from './project-converter';
// For testing
export { exampleRootTslintJson } from './example-tslint-configs';

View File

@ -0,0 +1,267 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
readJson,
readWorkspaceConfiguration,
Tree,
updateWorkspaceConfiguration,
writeJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { ProjectConverter } from './project-converter';
/**
* Don't run actual child_process implementation of installPackagesTask()
*/
jest.mock('child_process');
describe('ProjectConverter', () => {
let host: Tree;
const projectName = 'foo';
const projectRoot = `apps/${projectName}`;
beforeEach(async () => {
// Clean up any previous dry run simulations
process.argv = process.argv.filter(
(a) => !['--dry-run', '--dryRun', '-d'].includes(a)
);
host = createTreeWithEmptyWorkspace();
addProjectConfiguration(host, projectName, {
root: projectRoot,
projectType: 'application',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', `!${projectRoot}/**/*`],
tsConfig: [`${projectRoot}/tsconfig.app.json`],
},
},
},
/**
* PROJECT-LEVEL GENERATOR CONFIG - BEFORE CONVERSION
*
* Default set to tslint, using shorthand syntax
*/
generators: {
'@nrwl/angular:library': {
linter: 'tslint',
},
'@nrwl/angular:application': {
e2eTestRunner: 'cypress',
linter: 'eslint',
unitTestRunner: 'jest',
},
},
});
});
it('should throw if --dry-run is set', () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
process.argv.push('--dry-run');
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).toThrowErrorMatchingSnapshot();
});
it('should throw if --dryRun is set', () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
process.argv.push('--dryRun');
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).toThrowErrorMatchingSnapshot();
});
it('should throw if -d is set', () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
process.argv.push('-d');
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).toThrowErrorMatchingSnapshot();
});
it('should throw if no root tslint.json is found', () => {
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).toThrowErrorMatchingSnapshot();
});
it('should throw if no project tslint.json is found', () => {
writeJson(host, 'tslint.json', {});
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).toThrowErrorMatchingSnapshot();
});
it('should not throw when not in dry-run and config files successfully found', () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
expect(
() =>
new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
})
).not.toThrow();
});
describe('setDefaults()', () => {
it('should set the default configuration for removeTSLintIfNoMoreTSLintTargets in workspace.json', async () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
const projectConverter = new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
});
const workspace = readWorkspaceConfiguration(host);
workspace.generators = {
'@nrwl/angular': {
application: {
linter: 'tslint',
},
library: {
linter: 'tslint',
},
},
};
updateWorkspaceConfiguration(host, workspace);
// BEFORE - no entry for convert-tslint-to-eslint wthin @nrwl/angular generators
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
projectConverter.setDefaults('@nrwl/angular', true);
// AFTER (1) - convert-tslint-to-eslint wthin @nrwl/angular generators has removeTSLintIfNoMoreTSLintTargets set to true
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
projectConverter.setDefaults('@nrwl/angular', false);
// AFTER (2) - convert-tslint-to-eslint wthin @nrwl/angular generators has removeTSLintIfNoMoreTSLintTargets set to false
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
});
});
describe('removeTSLintFromWorkspace()', () => {
it('should remove all relevant traces of TSLint from the workspace', async () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
const projectConverter = new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
});
await addDependenciesToPackageJson(
host,
{},
{
codelyzer: 'latest',
tslint: 'latest',
}
)();
const workspace = readWorkspaceConfiguration(host);
// Not using shorthand syntax this time
workspace.generators = {
'@nrwl/angular': {
application: {
linter: 'tslint',
},
library: {
linter: 'tslint',
},
},
};
updateWorkspaceConfiguration(host, workspace);
// BEFORE - tslint and codelyzer are present
expect(readJson(host, 'package.json')).toMatchSnapshot();
// BEFORE - tslint set as both global and project-level default linter for @nrwl/angular generators
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
await projectConverter.removeTSLintFromWorkspace()();
// AFTER - it should remove tslint and codelyzer
expect(readJson(host, 'package.json')).toMatchSnapshot();
// AFTER - generators config from global and project-level settings removed (because eslint is always default)
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
});
it('should remove the entry in generators for convert-tslint-to-eslint because it is no longer needed', async () => {
writeJson(host, 'tslint.json', {});
writeJson(host, `${projectRoot}/tslint.json`, {});
const projectConverter = new ProjectConverter({
host,
projectName,
eslintInitializer: () => undefined,
});
const workspace = readWorkspaceConfiguration(host);
workspace.generators = {
'@nrwl/angular': {
'convert-tslint-to-eslint': {
removeTSLintIfNoMoreTSLintTargets: true,
},
},
};
updateWorkspaceConfiguration(host, workspace);
// BEFORE - convert-tslint-to-eslint wthin @nrwl/angular generators has a value for removeTSLintIfNoMoreTSLintTargets
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
await projectConverter.removeTSLintFromWorkspace()();
// AFTER - generators config no longer has a reference to convert-tslint-to-eslint
expect(readJson(host, 'workspace.json')).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,535 @@
import {
GeneratorCallback,
getPackageManagerCommand,
getProjects,
joinPathFragments,
logger,
NxJsonProjectConfiguration,
offsetFromRoot,
ProjectConfiguration,
readJson,
readProjectConfiguration,
readWorkspaceConfiguration,
removeDependenciesFromPackageJson,
Tree,
updateJson,
updateProjectConfiguration,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { detectPackageManager } from '@nrwl/tao/src/shared/package-manager';
import { execSync } from 'child_process';
import type { Linter } from 'eslint';
import { tslintToEslintConfigVersion } from '../versions';
import {
convertTSLintConfig,
ensureESLintPluginsAreInstalled,
deduplicateOverrides,
} from './utils';
/**
* Common schema used by all implementations of convert-tslint-to-eslint generators
*/
export interface ConvertTSLintToESLintSchema {
project: string;
removeTSLintIfNoMoreTSLintTargets: boolean;
}
/**
* When we convert a TSLint setup to an ESLint setup for a particular project, there are a number of
* shared/common concerns (implemented as library utilities within @nrwl/linter), and a few things
* which are specific to this package and the types of projects it produces.
*
* The key structure of the converted ESLint support is as follows:
*
* - We will first generate a workspace root .eslintrc.json which is the same as the one generated
* for new workspaces (i.e. it is NOT just a converted version of their root tslint.json). This allows us
* to have a consistent base for all users, as well as standardized patterns around "overrides".
*
* - The user's original root tslint.json will be converted and any applicable settings will be stored
* within ADDITIONAL override blocks within the root .eslintrc.json.
*
* - The user's project-level tslint.json file will be converted into a corresponding .eslintrc.json file
* and it will extend from the root workspace .eslintrc.json file as normal.
*/
export class ProjectConverter {
private readonly projectConfig: ProjectConfiguration &
NxJsonProjectConfiguration;
private readonly rootTSLintJsonPath = 'tslint.json';
private readonly rootTSLintJson: Record<string, unknown>;
private readonly projectTSLintJsonPath: string;
private readonly projectTSLintJson: Record<string, unknown>;
private readonly host: Tree;
private readonly projectName: string;
private readonly eslintInitializer: (projectInfo: {
projectName: string;
projectConfig: ProjectConfiguration & NxJsonProjectConfiguration;
}) => Promise<void>;
private readonly pmc: ReturnType<typeof getPackageManagerCommand>;
/**
* Using an object as the argument to the constructor means we sacrifice some
* authoring sugar around initializing these properties but it makes the usage
* of the class much easier to read and maintain.
*/
constructor({
host,
projectName,
eslintInitializer,
}: {
host: Tree;
projectName: string;
eslintInitializer: (projectInfo: {
projectName: string;
projectConfig: ProjectConfiguration & NxJsonProjectConfiguration;
}) => Promise<void>;
}) {
this.host = host;
this.projectName = projectName;
this.eslintInitializer = eslintInitializer;
this.projectConfig = readProjectConfiguration(this.host, this.projectName);
this.projectTSLintJsonPath = joinPathFragments(
this.projectConfig.root,
'tslint.json'
);
/**
* Given the user is converting a project from TSLint to ESLint, we expect them
* to have both a root and a project-specific tslint.json
*/
if (!host.exists(this.rootTSLintJsonPath)) {
throw new Error(
'We could not find a tslint.json at the root of your workspace, maybe you have already migrated to ESLint?'
);
}
if (!host.exists(this.projectTSLintJsonPath)) {
throw new Error(
`We could not find a tslint.json for the selected project "${this.projectTSLintJsonPath}", maybe you have already migrated to ESLint?`
);
}
this.rootTSLintJson = readJson(host, this.rootTSLintJsonPath);
this.projectTSLintJson = readJson(host, this.projectTSLintJsonPath);
const pm = detectPackageManager();
this.pmc = getPackageManagerCommand(pm);
/**
* We are not able to support --dry-run in this generator, because we need to dynamically install
* and use the tslint-to-eslint-config package within the same execution.
*
* This is a worthwhile trade-off and the dry-run output doesn't offer a ton of value for this use-case anyway.
*/
if (
process.argv.includes('--dry-run') ||
process.argv.includes('--dryRun') ||
process.argv.includes('-d')
) {
throw new Error(
'NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the "Run" button.\n'
);
}
}
/**
* In order to avoid all users of Nx needing to have tslint-to-eslint-config (and therefore tslint)
* in their node_modules, we dynamically install and uninstall the library as part of the conversion
* process.
*
* NOTE: By taking this approach we have to sacrifice dry-run capabilities for this generator.
*/
installTSLintToESLintConfigPackage() {
execSync(
`${this.pmc.addDev} tslint-to-eslint-config@${tslintToEslintConfigVersion}`,
{
cwd: this.host.root,
stdio: [0, 1, 2],
}
);
}
uninstallTSLintToESLintConfigPackage() {
execSync(`${this.pmc.rm} tslint-to-eslint-config`, {
cwd: this.host.root,
stdio: [0, 1, 2],
});
}
async initESLint() {
return this.eslintInitializer({
projectName: this.projectName,
projectConfig: this.projectConfig,
});
}
/**
* If the package-specific shareable config already exists then the workspace must already
* be part way through migrating from TSLint to ESLint. In this case we do not want to convert
* the root tslint.json again (and this utility will return a noop task), and we instead just
* focus on the project-level config conversion.
*/
async convertRootTSLintConfig(
applyPackageSpecificModifications: (json: Linter.Config) => Linter.Config
): Promise<Exclude<GeneratorCallback, void>> {
const convertedRoot = await convertTSLintConfig(
this.rootTSLintJson,
this.rootTSLintJsonPath,
[]
);
const convertedRootESLintConfig = convertedRoot.convertedESLintConfig;
/**
* Already set by Nx's shareable configs
*/
delete convertedRootESLintConfig.env;
delete convertedRootESLintConfig.parser;
delete convertedRootESLintConfig.parserOptions;
convertedRootESLintConfig.plugins = convertedRootESLintConfig.plugins.filter(
(p) =>
!p.startsWith('@angular-eslint') && !p.startsWith('@typescript-eslint')
);
/**
* The only piece of the converted root tslint.json that we need to pull out to
* apply to the existing overrides within the root .eslintrc.json is the
* @nrwl/nx/enforce-module-boundaries rule.
*/
const nxRuleName = '@nrwl/nx/enforce-module-boundaries';
const nxEnforceModuleBoundariesRule =
convertedRootESLintConfig.rules[nxRuleName];
if (nxEnforceModuleBoundariesRule) {
updateJson(this.host, '.eslintrc.json', (json) => {
if (!json.overrides) {
return json;
}
for (const override of json.overrides) {
if (!override.rules) {
continue;
}
if (!override.rules[nxRuleName]) {
continue;
}
override.rules[nxRuleName] = nxEnforceModuleBoundariesRule;
}
return json;
});
/**
* Remove it once we've used it on the root, so that is isn't applied
* to the package-specific shareable config
*/
delete convertedRootESLintConfig.rules[nxRuleName];
}
/**
* Update the root workspace .eslintrc.json with additional overrides
*/
const finalConvertedRootESLintConfig = applyPackageSpecificModifications(
convertedRootESLintConfig
);
updateJson(this.host, '.eslintrc.json', (json) => {
json.overrides = json.overrides || [];
if (
finalConvertedRootESLintConfig.overrides &&
finalConvertedRootESLintConfig.overrides.length
) {
json.overrides = [
...json.overrides,
...finalConvertedRootESLintConfig.overrides,
];
} else {
json.overrides.push({
files: ['*.ts'],
...finalConvertedRootESLintConfig,
});
}
json.overrides = deduplicateOverrides(json.overrides);
return json;
});
/**
* Through converting the config we may encounter TSLint rules whose closest
* equivalent in the ESLint ecosystem comes from a separate package/plugin.
*
* We therefore automatically install those extra packages for the user and
* explain that that's what we are doing.
*/
return ensureESLintPluginsAreInstalled(
this.host,
convertedRoot.ensureESLintPlugins
);
}
async convertProjectConfig(
applyPackageSpecificModifications: (json: Linter.Config) => Linter.Config
): Promise<GeneratorCallback> {
const convertedProjectConfig = await convertTSLintConfig(
this.projectTSLintJson,
this.projectTSLintJsonPath,
// Strip the extends on workspace tslint.json (see this util's docs for more info)
[`${offsetFromRoot(this.projectConfig.root)}tslint.json`]
);
const convertedProjectESLintConfig =
convertedProjectConfig.convertedESLintConfig;
/**
* Already set by Nx's shareable configs
*/
delete convertedProjectESLintConfig.env;
delete convertedProjectESLintConfig.parser;
delete convertedProjectESLintConfig.parserOptions;
convertedProjectESLintConfig.plugins = convertedProjectESLintConfig.plugins.filter(
(p) =>
!p.startsWith('@angular-eslint') && !p.startsWith('@typescript-eslint')
);
const projectESLintConfigPath = joinPathFragments(
this.projectConfig.root,
'.eslintrc.json'
);
/**
* Apply updates to the new .eslintrc.json file for the project
*/
updateJson(this.host, projectESLintConfigPath, (json) => {
if (typeof json.extends === 'string') {
json.extends = [json.extends];
}
// Custom extends from conversion
if (
Array.isArray(convertedProjectESLintConfig.extends) &&
convertedProjectESLintConfig.extends.length
) {
// Ignore any tslint-to-eslint-config default extends that do not apply to Nx
const applicableExtends = convertedProjectESLintConfig.extends.filter(
(ext) => !ext.startsWith('prettier')
);
if (applicableExtends.length) {
json.extends = [...json.extends, ...applicableExtends];
}
}
// Custom plugins from conversion
if (
Array.isArray(convertedProjectESLintConfig.plugins) &&
convertedProjectESLintConfig.plugins.length
) {
json.plugins = [
...json.plugins,
...convertedProjectESLintConfig.plugins,
];
}
/**
* Custom rules
*
* By default, tslint-to-eslint-config will try and apply any rules without known converters
* by using eslint-plugin-tslint. We instead explicitly warn the user about this missing converter,
* and therefore at this point we strip out any rules which start with @typescript-eslint/tslint/config
*/
json.rules = json.rules || {};
if (
convertedProjectESLintConfig.rules &&
Object.keys(convertedProjectESLintConfig.rules).length
) {
for (const [ruleName, ruleConfig] of Object.entries(
convertedProjectESLintConfig.rules
)) {
if (!ruleName.startsWith('@typescript-eslint/tslint/config')) {
// Prioritize the converted rules over any base implementations from the original Nx generator
json.rules[ruleName] = ruleConfig;
}
}
}
/**
* Apply any package-specific modifications to the converted config before
* updating the config file.
*/
const finalJson = applyPackageSpecificModifications(json);
return finalJson;
});
/**
* Through converting the config we may encounter TSLint rules whose closest
* equivalent in the ESLint ecosystem comes from a separate package/plugin.
*
* We therefore automatically install those extra packages for the user and
* explain that that's what we are doing.
*/
return ensureESLintPluginsAreInstalled(
this.host,
convertedProjectConfig.ensureESLintPlugins
);
}
removeProjectTSLintFile() {
this.host.delete(joinPathFragments(this.projectConfig.root, 'tslint.json'));
}
isTSLintUsedInWorkspace(): boolean {
const projects = getProjects(this.host);
for (const [, projectConfig] of projects.entries()) {
for (const [, targetConfig] of Object.entries(projectConfig.targets)) {
if (targetConfig.executor === '@angular-devkit/build-angular:tslint') {
// Workspace is still using TSLint, exit early
return true;
}
}
}
// If we got this far the user has no remaining TSLint usage
return false;
}
removeTSLintFromWorkspace(): GeneratorCallback {
logger.info(
`No TSLint usage will remain in the workspace, removing TSLint...`
);
/**
* Delete the root tslint.json
*/
this.host.delete(this.rootTSLintJsonPath);
/**
* Prepare the package.json and the uninstall task
*/
const uninstallTask = removeDependenciesFromPackageJson(
this.host,
[],
['tslint', 'codelyzer']
);
/**
* Update global linter configuration defaults in workspace.json
*/
const workspace = readWorkspaceConfiguration(this.host);
this.cleanUpGeneratorsConfig(workspace);
updateWorkspaceConfiguration(this.host, workspace);
/**
* Update project-level linter configuration defaults in workspace.json
*/
const projects = getProjects(this.host);
for (const [projectName, { generators }] of projects.entries()) {
if (!generators || Object.keys(generators).length === 0) {
continue;
}
const project = readProjectConfiguration(this.host, projectName);
this.cleanUpGeneratorsConfig(project);
updateProjectConfiguration(this.host, projectName, project);
}
return uninstallTask;
}
private cleanUpGeneratorsConfig(parentConfig: { generators?: any }) {
if (
!parentConfig.generators ||
Object.keys(parentConfig.generators).length === 0
) {
return;
}
for (const [collectionName, maybeGeneratorConfig] of Object.entries(
parentConfig.generators
)) {
// Shorthand syntax is possible
if (collectionName.includes(':')) {
const generatorConfig = maybeGeneratorConfig;
for (const optionName of Object.keys(generatorConfig)) {
if (optionName === 'linter') {
// Default is eslint, so in all cases we can just remove the config altogether
delete generatorConfig[optionName];
}
}
// If removing linter leaves no other options in the config, remove the config as well
if (Object.keys(generatorConfig).length === 0) {
delete parentConfig.generators[collectionName];
}
} else {
// Not shorthand syntax, so next level down is generator name -> config mapping
const collectionConfig = maybeGeneratorConfig;
for (const [generatorName, generatorConfig] of Object.entries(
collectionConfig
)) {
if (generatorName === 'convert-tslint-to-eslint') {
// No longer relevant because of TSLint is being removed the conversion process must be complete
delete collectionConfig[generatorName];
continue;
}
for (const optionName of Object.keys(generatorConfig)) {
if (optionName === 'linter') {
// Default is eslint, so in all cases we can just remove the config altogether
delete generatorConfig[optionName];
}
}
// If removing linter leaves no other options in the config, remove the generator config as well
if (Object.keys(generatorConfig).length === 0) {
delete collectionConfig[generatorName];
}
}
// If removing the generator leaves no other generators in the config, remove the config as well
if (
parentConfig.generators[collectionName] &&
Object.keys(parentConfig.generators[collectionName]).length === 0
) {
delete parentConfig.generators[collectionName];
}
}
}
// If removing the linter defaults leaves absolutely no generators configuration remaining, remove it
if (Object.keys(parentConfig.generators).length === 0) {
delete parentConfig.generators;
}
}
/**
* If the project which is the subject of the ProjectConverter instance is an application,
* figure out its associated e2e project's name.
*/
getE2EProjectName(): string | null {
if (this.projectConfig.projectType !== 'application') {
return null;
}
let e2eProjectName = null;
const projects = getProjects(this.host);
for (const [projectName, projectConfig] of projects.entries()) {
for (const [, targetConfig] of Object.entries(projectConfig.targets)) {
if (targetConfig.executor === '@nrwl/cypress:cypress') {
if (
targetConfig.options.devServerTarget === `${this.projectName}:serve`
) {
e2eProjectName = projectName;
logger.info(
`Found e2e project for "${this.projectName}" called "${e2eProjectName}", converting that project as well...`
);
}
}
}
}
return e2eProjectName;
}
setDefaults(
collectionName: string,
removeTSLintIfNoMoreTSLintTargets: ConvertTSLintToESLintSchema['removeTSLintIfNoMoreTSLintTargets']
) {
const workspace = readWorkspaceConfiguration(this.host);
workspace.generators = workspace.generators || {};
workspace.generators[collectionName] =
workspace.generators[collectionName] || {};
const prev = workspace.generators[collectionName];
workspace.generators = {
...workspace.generators,
[collectionName]: {
...prev,
'convert-tslint-to-eslint': {
removeTSLintIfNoMoreTSLintTargets,
},
},
};
updateWorkspaceConfiguration(this.host, workspace);
}
}

View File

@ -0,0 +1,58 @@
import type { Linter } from 'eslint';
import { deduplicateOverrides } from './utils';
describe('deduplicateOverrides()', () => {
it('should deduplicate overrides with identical values for "files"', () => {
const initialOverrides: Linter.Config['overrides'] = [
{
files: ['*.ts'],
env: {
foo: true,
},
rules: {
bar: 'error',
},
},
{
files: ['*.html'],
rules: {},
},
{
files: '*.ts',
plugins: ['wat'],
parserOptions: {
qux: false,
},
rules: {
bar: 'warn',
baz: 'error',
},
},
{
files: ['*.ts'],
extends: ['something'],
},
];
expect(deduplicateOverrides(initialOverrides)).toEqual([
{
files: ['*.ts'],
env: {
foo: true,
},
plugins: ['wat'],
extends: ['something'],
parserOptions: {
qux: false,
},
rules: {
bar: 'warn',
baz: 'error',
},
},
{
files: ['*.html'],
rules: {},
},
]);
});
});

View File

@ -0,0 +1,136 @@
import {
addDependenciesToPackageJson,
GeneratorCallback,
logger,
Tree,
} from '@nrwl/devkit';
import type { Linter } from 'eslint';
import type { TSLintRuleOptions } from 'tslint-to-eslint-config';
import { convertTslintNxRuleToEslintNxRule } from './convert-nx-enforce-module-boundaries-rule';
import { convertToESLintConfig } from './convert-to-eslint-config';
export function ensureESLintPluginsAreInstalled(
host: Tree,
eslintPluginsToBeInstalled: string[]
): GeneratorCallback {
if (!eslintPluginsToBeInstalled?.length) {
return () => undefined;
}
const additionalDevDependencies = {};
for (const pluginName of eslintPluginsToBeInstalled) {
additionalDevDependencies[pluginName] = 'latest';
}
logger.info(
'\nINFO: To most closely match your tslint.json, we will ensure the `latest` version of the following eslint plugin(s) are installed:'
);
logger.info('\n - ' + eslintPluginsToBeInstalled.join('\n - '));
logger.info(
'\nPlease note, you may later wish to pin these to a specific version number in your package.json, rather than leaving it open to `latest`.\n'
);
return addDependenciesToPackageJson(host, {}, additionalDevDependencies);
}
/**
* We don't want the user to depend on the TSLint fallback plugin, we will instead
* explicitly inform them of the rules that could not be converted automatically and
* advise them on what to do next.
*/
function warnInCaseOfUnconvertedRules(
tslintConfigPath: string,
unconvertedTSLintRules: TSLintRuleOptions[]
): void {
const unconvertedTSLintRuleNames = unconvertedTSLintRules
.filter(
// Ignore formatting related rules, they are handled by Nx format/prettier
(unconverted) =>
!['import-spacing', 'whitespace', 'typedef'].includes(
unconverted.ruleName
)
)
.map((unconverted) => unconverted.ruleName);
if (unconvertedTSLintRuleNames.length > 0) {
logger.warn(
`\nWARNING: Within "${tslintConfigPath}", the following ${unconvertedTSLintRuleNames.length} rule(s) did not have known converters in https://github.com/typescript-eslint/tslint-to-eslint-config`
);
logger.warn('\n - ' + unconvertedTSLintRuleNames.join('\n - '));
logger.warn(
'\nYou will need to decide on how to handle the above manually, but everything else has been handled for you automatically.\n'
);
}
}
export async function convertTSLintConfig(
rawTSLintJson: any,
tslintJsonPath: string,
ignoreExtendsVals: string[]
) {
const convertedProject = await convertToESLintConfig(
tslintJsonPath,
rawTSLintJson,
ignoreExtendsVals
);
/**
* Apply the custom converter for the nx-module-boundaries rule if applicable
*/
const convertedNxRule = convertTslintNxRuleToEslintNxRule(rawTSLintJson);
if (convertedNxRule) {
convertedProject.convertedESLintConfig.rules =
convertedProject.convertedESLintConfig.rules || {};
convertedProject.convertedESLintConfig.rules[convertedNxRule.ruleName] =
convertedNxRule.ruleConfig;
}
warnInCaseOfUnconvertedRules(
tslintJsonPath,
convertedProject.unconvertedTSLintRules
);
return convertedProject;
}
export function deduplicateOverrides(
overrides: Linter.Config['overrides'] = []
) {
const map = new Map();
for (const override of overrides) {
const mapKey: string =
typeof override.files === 'string'
? override.files
: override.files.join(',');
const existing: Set<Linter.ConfigOverride> = map.get(mapKey);
if (existing) {
existing.add(override);
map.set(mapKey, existing);
continue;
}
const set = new Set();
set.add(override);
map.set(mapKey, set);
}
let dedupedOverrides = [];
for (const [, overrides] of map.entries()) {
const overridesArr = Array.from(overrides);
if (overridesArr.length === 1) {
dedupedOverrides = [...dedupedOverrides, ...overridesArr];
continue;
}
let mergedOverride = {};
for (const override of overridesArr) {
mergedOverride = {
...mergedOverride,
...(override as any),
};
}
dedupedOverrides.push(mergedOverride);
}
return dedupedOverrides;
}

View File

@ -1,6 +1,7 @@
export const nxVersion = '*';
export const tslintVersion = '~6.1.0';
export const tslintToEslintConfigVersion = '2.2.0';
export const buildAngularVersion = '~0.1102.0';
export const typescriptESLintVersion = '4.3.0';

View File

@ -91,6 +91,18 @@
"factory": "./src/schematics/nestjs-schematics/nestjs-schematics#service",
"schema": "./src/schematics/nestjs-schematics/schema.json",
"description": "Run the 'service' NestJs generator with Nx project support"
},
"convert-tslint-to-eslint": {
"factory": "./src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint#conversionSchematic",
"schema": "./src/generators/convert-tslint-to-eslint/schema.json",
"description": "Convert a project from TSLint to ESLint"
}
},
"generators": {
"convert-tslint-to-eslint": {
"factory": "./src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint#conversionGenerator",
"schema": "./src/generators/convert-tslint-to-eslint/schema.json",
"description": "Convert a project from TSLint to ESLint"
}
}
}

View File

@ -30,6 +30,7 @@
},
"dependencies": {
"@nrwl/devkit": "*",
"@nrwl/linter": "*",
"@nrwl/node": "*",
"@nrwl/jest": "*",
"@angular-devkit/core": "~11.2.0",

View File

@ -0,0 +1,573 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convert-tslint-to-eslint should work for NestJS applications 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/eslint-plugin-nx": "*",
"@nrwl/linter": "*",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"eslint": "7.10.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "latest",
},
"name": "test-name",
}
`;
exports[`convert-tslint-to-eslint should work for NestJS applications 2`] = `
Object {
"projectType": "application",
"root": "apps/nest-app-1",
"targets": Object {
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/nest-app-1/**/*.ts",
],
},
},
},
}
`;
exports[`convert-tslint-to-eslint should work for NestJS applications 3`] = `
Object {
"ignorePatterns": Array [
"**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {
"@nrwl/nx/enforce-module-boundaries": Array [
"error",
Object {
"allow": Array [
"@nx-example/shared/product/data/testing",
],
"depConstraints": Array [
Object {
"onlyDependOnLibsWithTags": Array [
"type:feature",
"type:ui",
],
"sourceTag": "type:app",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:ui",
"type:data",
"type:types",
"type:state",
],
"sourceTag": "type:feature",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:types",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:state",
"type:types",
"type:data",
],
"sourceTag": "type:state",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:data",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:e2e-utils",
],
"sourceTag": "type:e2e",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
"type:ui",
],
"sourceTag": "type:ui",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:products",
"scope:shared",
],
"sourceTag": "scope:products",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:cart",
"scope:shared",
],
"sourceTag": "scope:cart",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/typescript",
],
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/javascript",
],
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
],
"plugins": Array [
"eslint-plugin-import",
],
"rules": Object {
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"_buffer",
"_counters",
"_timers",
"_groupDepth",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
],
"plugins": Array [
"@nrwl/nx",
],
"root": true,
}
`;
exports[`convert-tslint-to-eslint should work for NestJS applications 4`] = `
Object {
"extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"parserOptions": Object {
"project": Array [
"apps/nest-app-1/tsconfig.*?.json",
],
},
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
],
"rules": Object {
"@typescript-eslint/no-empty-interface": "error",
},
}
`;
exports[`convert-tslint-to-eslint should work for NestJS libraries 1`] = `
Object {
"dependencies": Object {},
"devDependencies": Object {
"@nrwl/eslint-plugin-nx": "*",
"@nrwl/linter": "*",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"eslint": "7.10.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "latest",
},
"name": "test-name",
}
`;
exports[`convert-tslint-to-eslint should work for NestJS libraries 2`] = `
Object {
"projectType": "library",
"root": "libs/nest-lib-1",
"targets": Object {
"lint": Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"libs/nest-lib-1/**/*.ts",
],
},
},
},
}
`;
exports[`convert-tslint-to-eslint should work for NestJS libraries 3`] = `
Object {
"ignorePatterns": Array [
"**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": Object {
"@nrwl/nx/enforce-module-boundaries": Array [
"error",
Object {
"allow": Array [
"@nx-example/shared/product/data/testing",
],
"depConstraints": Array [
Object {
"onlyDependOnLibsWithTags": Array [
"type:feature",
"type:ui",
],
"sourceTag": "type:app",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:ui",
"type:data",
"type:types",
"type:state",
],
"sourceTag": "type:feature",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:types",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:state",
"type:types",
"type:data",
],
"sourceTag": "type:state",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
],
"sourceTag": "type:data",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:e2e-utils",
],
"sourceTag": "type:e2e",
},
Object {
"onlyDependOnLibsWithTags": Array [
"type:types",
"type:ui",
],
"sourceTag": "type:ui",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:products",
"scope:shared",
],
"sourceTag": "scope:products",
},
Object {
"onlyDependOnLibsWithTags": Array [
"scope:cart",
"scope:shared",
],
"sourceTag": "scope:cart",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/typescript",
],
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/javascript",
],
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
],
"plugins": Array [
"eslint-plugin-import",
],
"rules": Object {
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": Array [
"off",
Object {
"accessibility": "explicit",
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
"error",
Object {
"ignoreParameters": true,
},
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": Array [
"error",
Object {
"hoist": "all",
},
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"constructor-super": "error",
"eqeqeq": Array [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-console": Array [
"error",
Object {
"allow": Array [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"_buffer",
"_counters",
"_timers",
"_groupDepth",
],
},
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": Array [
"error",
"rxjs/Rx",
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"prefer-const": "error",
"radix": "error",
},
},
],
"plugins": Array [
"@nrwl/nx",
],
"root": true,
}
`;
exports[`convert-tslint-to-eslint should work for NestJS libraries 4`] = `
Object {
"extends": Array [
"../../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"files": Array [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"parserOptions": Object {
"project": Array [
"libs/nest-lib-1/tsconfig.*?.json",
],
},
"rules": Object {},
},
Object {
"files": Array [
"*.ts",
"*.tsx",
],
"rules": Object {},
},
Object {
"files": Array [
"*.js",
"*.jsx",
],
"rules": Object {},
},
],
"rules": Object {
"@typescript-eslint/no-empty-interface": "error",
},
}
`;

View File

@ -0,0 +1,235 @@
import {
addProjectConfiguration,
joinPathFragments,
readJson,
readProjectConfiguration,
Tree,
writeJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { exampleRootTslintJson } from '@nrwl/linter';
import { conversionGenerator } from './convert-tslint-to-eslint';
/**
* Don't run actual child_process implementation of installPackagesTask()
*/
jest.mock('child_process');
const appProjectName = 'nest-app-1';
const appProjectRoot = `apps/${appProjectName}`;
const appProjectTSLintJsonPath = joinPathFragments(
appProjectRoot,
'tslint.json'
);
const libProjectName = 'nest-lib-1';
const libProjectRoot = `libs/${libProjectName}`;
const libProjectTSLintJsonPath = joinPathFragments(
libProjectRoot,
'tslint.json'
);
// Used to configure the test Tree and stub the response from tslint-to-eslint-config util findReportedConfiguration()
const projectTslintJsonData = {
raw: {
extends: '../../tslint.json',
rules: {
// User custom TS rule
'no-empty-interface': true,
// User custom rule with no known automated converter
'some-super-custom-rule-with-no-converter': true,
},
linterOptions: {
exclude: ['!**/*'],
},
},
tslintPrintConfigResult: {
rules: {
'no-empty-interface': {
ruleArguments: [],
ruleSeverity: 'error',
},
'some-super-custom-rule-with-no-converter': {
ruleArguments: [],
ruleSeverity: 'error',
},
},
},
};
function mockFindReportedConfiguration(_, pathToTslintJson) {
switch (pathToTslintJson) {
case 'tslint.json':
return exampleRootTslintJson.tslintPrintConfigResult;
case appProjectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
case libProjectTSLintJsonPath:
return projectTslintJsonData.tslintPrintConfigResult;
default:
throw new Error(
`mockFindReportedConfiguration - Did not recognize path ${pathToTslintJson}`
);
}
}
/**
* See ./mock-tslint-to-eslint-config.ts for why this is needed
*/
jest.mock('tslint-to-eslint-config', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('tslint-to-eslint-config'),
findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
};
});
/**
* Mock the the mutating fs utilities used within the conversion logic, they are not
* needed because of our stubbed response for findReportedConfiguration() above, and
* they would cause noise in the git data of the actual Nx repo when the tests run.
*/
jest.mock('fs', () => {
return {
// Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
// @ts-ignore
...jest.requireActual('fs'),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
};
});
describe('convert-tslint-to-eslint', () => {
let host: Tree;
beforeEach(async () => {
host = createTreeWithEmptyWorkspace();
writeJson(host, 'tslint.json', exampleRootTslintJson.raw);
addProjectConfiguration(host, appProjectName, {
root: appProjectRoot,
projectType: 'application',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!apps/nest-app-1/**/*'],
tsConfig: ['apps/nest-app-1/tsconfig.app.json'],
},
},
},
});
addProjectConfiguration(host, libProjectName, {
root: libProjectRoot,
projectType: 'library',
targets: {
/**
* LINT TARGET CONFIG - BEFORE CONVERSION
*
* TSLint executor configured for the project
*/
lint: {
executor: '@angular-devkit/build-angular:tslint',
options: {
exclude: ['**/node_modules/**', '!libs/nest-lib-1/**/*'],
tsConfig: ['libs/nest-lib-1/tsconfig.app.json'],
},
},
},
});
/**
* Existing tslint.json file for the app project
*/
writeJson(host, 'apps/nest-app-1/tslint.json', projectTslintJsonData.raw);
/**
* Existing tslint.json file for the lib project
*/
writeJson(host, 'libs/nest-lib-1/tslint.json', projectTslintJsonData.raw);
});
it('should work for NestJS applications', async () => {
await conversionGenerator(host, {
project: appProjectName,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, appProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
expect(readJson(host, '.eslintrc.json')).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(appProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(appProjectTSLintJsonPath)).toEqual(false);
});
it('should work for NestJS libraries', async () => {
await conversionGenerator(host, {
project: libProjectName,
removeTSLintIfNoMoreTSLintTargets: false,
});
/**
* It should ensure the required Nx packages are installed and available
*
* NOTE: tslint-to-eslint-config should NOT be present
*/
expect(readJson(host, 'package.json')).toMatchSnapshot();
/**
* LINT TARGET CONFIG - AFTER CONVERSION
*
* It should replace the TSLint executor with the ESLint one
*/
expect(readProjectConfiguration(host, libProjectName)).toMatchSnapshot();
/**
* The root level .eslintrc.json should now have been generated
*/
expect(readJson(host, '.eslintrc.json')).toMatchSnapshot();
/**
* The project level .eslintrc.json should now have been generated
* and extend from the root, as well as applying any customizations
* which are specific to this projectType.
*/
expect(
readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
).toMatchSnapshot();
/**
* The project's TSLint file should have been deleted
*/
expect(host.exists(libProjectTSLintJsonPath)).toEqual(false);
});
});

View File

@ -0,0 +1,134 @@
import {
convertNxGenerator,
formatFiles,
GeneratorCallback,
Tree,
} from '@nrwl/devkit';
import { ConvertTSLintToESLintSchema, ProjectConverter } from '@nrwl/linter';
import {
addLintingToApplication,
NormalizedSchema as AddLintForApplicationSchema,
} from '@nrwl/node/src/generators/application/application';
import {
addLint as addLintingToLibraryGenerator,
NormalizedSchema as AddLintForLibrarySchema,
} from '@nrwl/workspace/src/generators/library/library';
import type { Linter } from 'eslint';
export async function conversionGenerator(
host: Tree,
options: ConvertTSLintToESLintSchema
) {
/**
* The ProjectConverter instance encapsulates all the standard operations we need
* to perform in order to convert a project from TSLint to ESLint, as well as some
* extensibility points for adjusting the behavior on a per package basis.
*
* E.g. @nrwl/angular projects might need to make different changes to the final
* ESLint config when compared with @nrwl/nest projects.
*
* See the ProjectConverter implementation for a full breakdown of what it does.
*/
const projectConverter = new ProjectConverter({
host,
projectName: options.project,
eslintInitializer: async ({ projectName, projectConfig }) => {
/**
* Using .js is not an option with NestJS, so we always set it to false when
* delegating to the external (more generic) generators below.
*/
const js = false;
if (projectConfig.projectType === 'application') {
await addLintingToApplication(host, {
linter: 'eslint' as any,
name: projectName,
appProjectRoot: projectConfig.root,
js,
} as AddLintForApplicationSchema);
}
if (projectConfig.projectType === 'library') {
await addLintingToLibraryGenerator(host, {
linter: 'eslint',
name: projectName,
projectRoot: projectConfig.root,
js,
} as AddLintForLibrarySchema);
}
},
});
/**
* Dynamically install tslint-to-eslint-config to assist with the conversion.
*/
projectConverter.installTSLintToESLintConfigPackage();
/**
* Create the standard (which is applicable to the current package) ESLint setup
* for converting the project.
*/
await projectConverter.initESLint();
/**
* Convert the root tslint.json and apply the converted rules to the root .eslintrc.json.
*/
const rootConfigInstallTask = await projectConverter.convertRootTSLintConfig(
(json) => removeCodelyzerRelatedRules(json)
);
/**
* Convert the project's tslint.json to an equivalent ESLint config.
*/
const projectConfigInstallTask = await projectConverter.convertProjectConfig(
(json) => json
);
/**
* Clean up the original TSLint configuration for the project.
*/
projectConverter.removeProjectTSLintFile();
/**
* Store user preference regarding removeTSLintIfNoMoreTSLintTargets for the collection
*/
projectConverter.setDefaults(
'@nrwl/nest',
options.removeTSLintIfNoMoreTSLintTargets
);
/**
* Based on user preference and remaining usage, remove TSLint from the workspace entirely.
*/
let uninstallTSLintTask: GeneratorCallback = () => Promise.resolve(undefined);
if (
options.removeTSLintIfNoMoreTSLintTargets &&
!projectConverter.isTSLintUsedInWorkspace()
) {
uninstallTSLintTask = projectConverter.removeTSLintFromWorkspace();
}
await formatFiles(host);
return async () => {
projectConverter.uninstallTSLintToESLintConfigPackage();
await rootConfigInstallTask();
await projectConfigInstallTask();
await uninstallTSLintTask();
};
}
export const conversionSchematic = convertNxGenerator(conversionGenerator);
/**
* Remove any @angular-eslint rules that were applied as a result of converting prior codelyzer
* rules, because they are only relevant for Angular projects.
*/
function removeCodelyzerRelatedRules(json: Linter.Config): Linter.Config {
for (const ruleName of Object.keys(json.rules)) {
if (ruleName.startsWith('@angular-eslint')) {
delete json.rules[ruleName];
}
}
return json;
}

View File

@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/schema",
"id": "nest-convert-tslint-to-eslint",
"cli": "nx",
"title": "Convert a NestJS project from TSLint to ESLint",
"description": "NOTE: Does not work in --dry-run mode",
"examples": [
{
"command": "g convert-tslint-to-eslint myapp",
"description": "Convert the NestJS project `myapp` from TSLint to ESLint"
}
],
"type": "object",
"properties": {
"project": {
"description": "The name of the NestJS project to convert.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Which NestJS project would you like to convert from TSLint to ESLint?"
},
"removeTSLintIfNoMoreTSLintTargets": {
"type": "boolean",
"description": "If this conversion leaves no more TSLint usage in the workspace, it will remove TSLint and related dependencies and configuration",
"default": true,
"x-prompt": "Would you like to remove TSLint and its related config if there are no TSLint projects remaining after this conversion?"
}
},
"required": ["project"]
}

View File

@ -30,7 +30,7 @@ import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-ser
import { Schema } from './schema';
import { initGenerator } from '../init/init';
interface NormalizedSchema extends Schema {
export interface NormalizedSchema extends Schema {
appProjectRoot: string;
parsedTags: string[];
}
@ -157,6 +157,25 @@ function addProxy(tree: Tree, options: NormalizedSchema) {
}
}
export async function addLintingToApplication(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const lintTask = await lintProjectGenerator(tree, {
linter: options.linter,
project: options.name,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
eslintFilePatterns: [
`${options.appProjectRoot}/**/*.${options.js ? 'js' : 'ts'}`,
],
skipFormat: true,
});
return lintTask;
}
export async function applicationGenerator(tree: Tree, schema: Schema) {
const options = normalizeOptions(tree, schema);
@ -171,15 +190,7 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
addProject(tree, options);
if (options.linter !== Linter.None) {
const lintTask = await lintProjectGenerator(tree, {
linter: options.linter,
project: options.name,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
eslintFilePatterns: [`${options.appProjectRoot}/**/*.ts`],
skipFormat: true,
});
const lintTask = await addLintingToApplication(tree, options);
tasks.push(lintTask);
}

View File

@ -38,7 +38,7 @@ function addProject(tree: Tree, options: NormalizedSchema) {
});
}
function addLint(
export function addLint(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {

View File

@ -27,7 +27,13 @@ const IGNORE_MATCHES = {
'identity-obj-proxy',
'@angular-devkit/schematics',
],
linter: ['eslint', '@angular-devkit/schematics', '@angular-devkit/architect'],
linter: [
'eslint',
'@angular-devkit/schematics',
'@angular-devkit/architect',
// Installed and uninstalled dynamically when the conversion generator runs
'tslint-to-eslint-config',
],
next: [
'@angular-devkit/architect',
'@nrwl/devkit',

View File

@ -28,6 +28,8 @@
"@nrwl/express": ["./packages/express"],
"@nrwl/nest": ["./packages/nest"],
"@nrwl/next": ["./packages/next"],
"@nrwl/node": ["./packages/node"],
"@nrwl/node/*": ["./packages/node/*"],
"@nrwl/linter": ["./packages/linter"],
"@nrwl/jest": ["./packages/jest"],
"@nrwl/workspace/testing": ["./packages/workspace/testing"],

View File

@ -8124,6 +8124,11 @@ codelyzer@~5.0.1:
source-map "^0.5.7"
sprintf-js "^1.1.2"
coffeescript@1.12.7:
version "1.12.7"
resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.12.7.tgz#e57ee4c4867cf7f606bfc4a0f2d550c0981ddd27"
integrity sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@ -8219,6 +8224,11 @@ commander@3.0.2:
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
commander@7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -8999,6 +9009,13 @@ crypto-random-string@^1.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
cson-parser@4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/cson-parser/-/cson-parser-4.0.8.tgz#373f3bde1be018267fccbc575d82120c2a7a645e"
integrity sha512-Hdv3N2E5JU4vAp88cxcs/Y+0L0y0HJnpoc067E//qbXNF4/cG713rFLryD0QvKZYK6w3QBA67t7UOfo2ymh8Sg==
dependencies:
coffeescript "1.12.7"
css-color-keywords@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
@ -10563,7 +10580,7 @@ escodegen@^1.11.1, escodegen@^1.14.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@^8.1.0:
eslint-config-prettier@8.1.0, eslint-config-prettier@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6"
integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==
@ -14452,6 +14469,13 @@ json3@^3.3.2, json3@^3.3.3:
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
json5@2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
dependencies:
minimist "^1.2.5"
json5@2.x, json5@^2.1.1, json5@^2.1.2, json5@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
@ -15209,6 +15233,11 @@ lodash@4.17.20, lodash@^4.17.20:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lodash@4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.5.0:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
@ -21932,6 +21961,22 @@ tslib@^1.13.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslint-to-eslint-config@2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/tslint-to-eslint-config/-/tslint-to-eslint-config-2.2.0.tgz#711a1596b765175139193a3878a8a12ebdf91318"
integrity sha512-ta+V1G8y431CPXuJHbzlYYxuAyvKZM8llLZnFN7jy0C98dMsz0jIQCZW7dH5I6wt10gTyMOw7h5W+JEeQumbfQ==
dependencies:
chalk "4.1.0"
commander "7.1.0"
cson-parser "4.0.8"
eslint-config-prettier "8.1.0"
glob "7.1.6"
json5 "2.2.0"
lodash "4.17.21"
minimatch "3.0.4"
tslint "6.1.3"
typescript "4.2.2"
tslint@6.1.3:
version "6.1.3"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
@ -22076,6 +22121,11 @@ typescript@4.1.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
typescript@4.2.2:
version "4.2.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c"
integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==
uglify-js@3.4.x:
version "3.4.10"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"