Skip to content

Latest commit

 

History

History
485 lines (426 loc) · 24.8 KB

MIGRATING_FROM_TSLINT.md

File metadata and controls

485 lines (426 loc) · 24.8 KB

Migrating from TSLint: Current Status as of angular-eslint v16

TSLint was deprecated by its creators all the way back in 2019: palantir/tslint#4534

The Angular CLI stopped supporting their TSLint builder implementation (to power ng lint) as of version 13, which is now 3 (or maybe more depending on when you are reading this) major versions ago, meaning it is at least 1.5 years ago.

During the initial couple of years angular-eslint was delighted to be able to provide valuable tooling to help with a mostly automated transition from TSLint to ESLint for Angular CLI projects.

As a community project we need to focus on what adds the most value to the majority of our users, and so, in version 16, this conversion tooling was removed.

If you want to leverage it, you can of course use it on a major version of Angular + angular-eslint prior to version 16, or you can feel free to take the implementation from our git history and leverage the schematics inline in your own projects.

If you are looking for general help in migrating specific rules from TSLint to ESLint, you can check out this incredible project that we depended on in our conversion schematic: https://github.com/typescript-eslint/tslint-to-eslint-config

Our original guide and Codelyzer equivalence table, both of which correspond to angular-eslint version 15, can be found below, in addition to the legacy ng-cli-compat and ng-cli-compat-formatting-add-on configs.




Migrating an Angular CLI project from Codelyzer and TSLint

We have some tooling to make this as automated as possible, but the reality is it will always be somewhat project-specific as to how much work will be involved in the migration.

Step 1 - Add relevant dependencies

The first step is to run the schematic to add @angular-eslint to your project:

ng add @angular-eslint/schematics

This will handle installing the latest version of all the relevant packages for you and adding them to the devDependencies of your package.json.

Step 2 - Run the convert-tslint-to-eslint schematic on a project

If you just have a single project in your workspace you can just run:

ng g @angular-eslint/schematics:convert-tslint-to-eslint

If you have a projects/ directory or similar in your workspace, you will have multiple entries in your projects configuration and you will need to chose which one you want to migrate using the convert-tslint-to-eslint schematic:

ng g @angular-eslint/schematics:convert-tslint-to-eslint {{YOUR_PROJECT_NAME_GOES_HERE}}

The schematic will do the following for you:

  • Read your chosen project's tslint.json and use it to CREATE a .eslintrc.json at the root of the specific project which extends from the root config (if you do not already have a root config, it will also add one automatically for you).
    • The contents of this .eslintrc.json will be the closest possible equivalent to your tslint.json that the tooling can figure out.
    • You will want to pay close attention to the terminal output of the schematic as it runs, because it will let you know if it couldn't find an appropriate converter for a TSLint rule, or if it has installed any additional ESLint plugins to help you match up your new setup with your old one.
  • UPDATE the project's architect configuration in the angular.json to such that the lint "target" will invoke ESLint instead of TSLint.
  • UPDATE any instances of tslint:disable comments that are located within your TypeScript source files to their ESLint equivalent.
  • If you choose YES (the default) for the --remove-tslint-if-no-more-tslint-targets option, it will also automatically remove TSLint and Codelyzer from your workspace if you have no more usage of them left.

Now when you run:

npx ng lint {{YOUR_PROJECT_NAME_GOES_HERE}}

...you are running ESLint on your project! 🎉


Status of Codelyzer Rules Conversion

The table below shows the status of each Codelyzer Rule in terms of whether or not an equivalent for it has been created within @angular-eslint.

If you see a rule below that has no status against it, then please feel free to open a PR with an appropriate implementation. You can look at the Codelyzer repo and the existing plugins within this repo for inspiration.

Explanation of Statuses
✅ = We have created an ESLint equivalent of this TSLint rule
🚧 = There is an open PR to provide an ESLint equivalent of this TSLint rule
🙅 = This TSLint rule has been replaced by functionality within the Angular compiler, or should be replaced by a dedicated code formatter, such as Prettier

Functionality

Codelyzer Rule ESLint Equivalent Status
contextual-decorator @angular-eslint/contextual-decorator
contextual-lifecycle @angular-eslint/contextual-lifecycle
no-attribute-decorator @angular-eslint/no-attribute-decorator
no-lifecycle-call @angular-eslint/no-lifecycle-call
no-output-native @angular-eslint/no-output-native
no-pipe-impure @angular-eslint/no-pipe-impure
prefer-on-push-component-change-detection @angular-eslint/prefer-on-push-component-change-detection
template-accessibility-alt-text @angular-eslint/template/accessibility-alt-text
template-accessibility-elements-content @angular-eslint/template/accessibility-elements-content
template-accessibility-label-for @angular-eslint/template/accessibility-label-for
template-accessibility-tabindex-no-positive @angular-eslint/template/no-positive-tabindex
template-accessibility-table-scope @angular-eslint/template/accessibility-table-scope
template-accessibility-valid-aria @angular-eslint/template/accessibility-valid-aria
template-banana-in-box @angular-eslint/template/banana-in-box
template-click-events-have-key-events @angular-eslint/template/click-events-have-key-events
template-mouse-events-have-key-events @angular-eslint/template/mouse-events-have-key-events
template-no-any @angular-eslint/template/no-any
template-no-autofocus @angular-eslint/template/no-autofocus
template-no-distracting-elements @angular-eslint/template/no-distracting-elements
template-no-negated-async @angular-eslint/template/no-negated-async
use-injectable-provided-in @angular-eslint/use-injectable-provided-in
use-lifecycle-interface @angular-eslint/use-lifecycle-interface

Maintainability

Codelyzer Rule ESLint Equivalent Status
component-max-inline-declarations @angular-eslint/component-max-inline-declarations
no-conflicting-lifecycle @angular-eslint/no-conflicting-lifecycle
no-forward-ref @angular-eslint/no-forward-ref
no-input-prefix @angular-eslint/no-input-prefix
no-input-rename @angular-eslint/no-input-rename
no-output-on-prefix @angular-eslint/no-output-on-prefix
no-output-rename @angular-eslint/no-output-rename
no-unused-css
prefer-output-readonly @angular-eslint/prefer-output-readonly
relative-url-prefix @angular-eslint/relative-url-prefix
template-conditional-complexity @angular-eslint/template/conditional-complexity
template-cyclomatic-complexity @angular-eslint/template/cyclomatic-complexity
template-i18n @angular-eslint/template/i18n
template-no-call-expression @angular-eslint/template/no-call-expression
template-use-track-by-function @angular-eslint/template/use-track-by-function
use-component-selector @angular-eslint/use-component-selector
use-component-view-encapsulation @angular-eslint/use-component-view-encapsulation
use-pipe-decorator N/A, see explanation above 🙅
use-pipe-transform-interface @angular-eslint/use-pipe-transform-interface

Style

Codelyzer Rule ESLint Equivalent Status
angular-whitespace N/A, see explanation above 🙅
component-class-suffix @angular-eslint/component-class-suffix
component-selector @angular-eslint/component-selector
directive-class-suffix @angular-eslint/directive-class-suffix
directive-selector @angular-eslint/directive-selector
import-destructuring-spacing N/A, see explanation above 🙅
no-host-metadata-property @angular-eslint/no-host-metadata-property
no-inputs-metadata-property @angular-eslint/no-inputs-metadata-property
no-outputs-metadata-property @angular-eslint/no-outputs-metadata-property
no-queries-metadata-property @angular-eslint/no-queries-metadata-property
pipe-prefix @angular-eslint/pipe-prefix
prefer-inline-decorator N/A, see explanation above 🙅



Legacy premade ESLint configs

We strongly encourage migrating to extend from the recommended configs from both typescript-eslint and angular-eslint as soon as possible.

ng-cli-compat and ng-cli-compat--formatting-add-on

If you ever used the convert-tslint-to-eslint schematic in the past, you might have noticed that it generated a config which extended from ng-cli-compat and ng-cli-compat--formatting-add-on.

As you might infer from the names, these configs existed to most closely match what the Angular CLI used to configure for TSLint and help us reduce a lot of the boilerplate config as part of the TSLint -> ESLint conversion.

You are free to remove them or customize them in any way you wish. Over time, we will encourage people more and more to move towards the recommended config instead, because this will not be static, it will evolve as recommendations from the Angular Team and community do.

Note: The equivalent TSLint config from the Angular CLI = both ng-cli-compat + ng-cli-compat--formatting-add-on.

The reason for separating out the formatting related rules was that we fundamentally believe you should not use a linter for formatting concerns (you should use a dedicated code formatting tool like Prettier instead), and having them in a separate config that is extended from makes it super easy to remove if you choose to.

We strongly encourage migrating to extend from the recommended configs from both typescript-eslint and angular-eslint as soon as possible. These configs are kept up to date as recommendations across the various ecosystems evolve.

If you would like to recreate the ng-cli-compat and ng-cli-compat--formatting-add-on configs as they exists in v15 and earlier, then you can use the following:

ng-cli-compat

{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "@angular-eslint"],
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "plugins": [
    "eslint-plugin-import",
    "eslint-plugin-jsdoc",
    "eslint-plugin-prefer-arrow"
  ],
  "rules": {
    "@typescript-eslint/interface-name-prefix": "off",
    "@typescript-eslint/explicit-member-accessibility": "off",
    "sort-keys": "off",
    "@angular-eslint/component-class-suffix": "error",
    "@angular-eslint/component-selector": [
      "error",
      {
        "type": "element",
        "prefix": "app",
        "style": "kebab-case"
      }
    ],
    "@angular-eslint/contextual-lifecycle": "error",
    "@angular-eslint/directive-class-suffix": "error",
    "@angular-eslint/directive-selector": [
      "error",
      {
        "type": "attribute",
        "prefix": "app",
        "style": "camelCase"
      }
    ],
    "@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/adjacent-overload-signatures": "error",
    "@typescript-eslint/array-type": "off",
    "@typescript-eslint/ban-types": [
      "error",
      {
        "types": {
          "Object": {
            "message": "Avoid using the `Object` type. Did you mean `object`?"
          },
          "Function": {
            "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
          },
          "Boolean": {
            "message": "Avoid using the `Boolean` type. Did you mean `boolean`?"
          },
          "Number": {
            "message": "Avoid using the `Number` type. Did you mean `number`?"
          },
          "String": {
            "message": "Avoid using the `String` type. Did you mean `string`?"
          },
          "Symbol": {
            "message": "Avoid using the `Symbol` type. Did you mean `symbol`?"
          }
        }
      }
    ],
    "@typescript-eslint/consistent-type-assertions": "error",
    "@typescript-eslint/dot-notation": "error",
    "@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-explicit-any": "off",
    "@typescript-eslint/no-inferrable-types": [
      "error",
      {
        "ignoreParameters": true
      }
    ],
    "@typescript-eslint/no-misused-new": "error",
    "@typescript-eslint/no-namespace": "error",
    "@typescript-eslint/no-non-null-assertion": "error",
    "@typescript-eslint/no-parameter-properties": "off",
    "@typescript-eslint/no-unused-expressions": "error",
    "@typescript-eslint/no-use-before-define": "off",
    "@typescript-eslint/no-var-requires": "off",
    "@typescript-eslint/prefer-for-of": "error",
    "@typescript-eslint/prefer-function-type": "error",
    "@typescript-eslint/prefer-namespace-keyword": "error",
    "@typescript-eslint/triple-slash-reference": [
      "error",
      {
        "path": "always",
        "types": "prefer-import",
        "lib": "always"
      }
    ],
    "@typescript-eslint/unified-signatures": "error",
    "complexity": "off",
    "constructor-super": "error",
    "eqeqeq": ["error", "smart"],
    "guard-for-in": "error",
    "id-blacklist": [
      "error",
      "any",
      "Number",
      "number",
      "String",
      "string",
      "Boolean",
      "boolean",
      "Undefined",
      "undefined"
    ],
    "id-match": "error",
    "import/no-deprecated": "warn",
    "jsdoc/newline-after-description": "error",
    "jsdoc/no-types": "error",
    "max-classes-per-file": "off",
    "no-bitwise": "error",
    "no-caller": "error",
    "no-cond-assign": "error",
    "no-console": [
      "error",
      {
        "allow": [
          "log",
          "warn",
          "dir",
          "timeLog",
          "assert",
          "clear",
          "count",
          "countReset",
          "group",
          "groupEnd",
          "table",
          "dirxml",
          "error",
          "groupCollapsed",
          "Console",
          "profile",
          "profileEnd",
          "timeStamp",
          "context"
        ]
      }
    ],
    "no-debugger": "error",
    "no-empty": "off",
    "no-eval": "error",
    "no-fallthrough": "error",
    "no-invalid-this": "off",
    "no-new-wrappers": "error",
    "no-restricted-imports": [
      "error",
      {
        "name": "rxjs/Rx",
        "message": "Please import directly from 'rxjs' instead"
      }
    ],
    "@typescript-eslint/no-shadow": [
      "error",
      {
        "hoist": "all"
      }
    ],
    "no-throw-literal": "error",
    "no-undef-init": "error",
    "no-underscore-dangle": "error",
    "no-unsafe-finally": "error",
    "no-unused-labels": "error",
    "no-var": "error",
    "object-shorthand": "error",
    "one-var": ["error", "never"],
    "prefer-arrow/prefer-arrow-functions": "error",
    "prefer-const": "error",
    "radix": "error",
    "use-isnan": "error",
    "valid-typeof": "off"
  }
}

ng-cli-compat--formatting-add-on

{
  "plugins": ["eslint-plugin-jsdoc"],
  "rules": {
    "arrow-body-style": "error",
    "arrow-parens": "off",
    "comma-dangle": "off",
    "curly": "error",
    "eol-last": "error",
    "jsdoc/check-alignment": "error",
    "max-len": [
      "error",
      {
        "code": 140
      }
    ],
    "new-parens": "error",
    "no-multiple-empty-lines": "off",
    "no-trailing-spaces": "error",
    "quote-props": ["error", "as-needed"],
    "space-before-function-paren": [
      "error",
      {
        "anonymous": "never",
        "asyncArrow": "always",
        "named": "never"
      }
    ],
    "@typescript-eslint/member-delimiter-style": [
      "error",
      {
        "multiline": {
          "delimiter": "semi",
          "requireLast": true
        },
        "singleline": {
          "delimiter": "semi",
          "requireLast": false
        }
      }
    ],
    "quotes": "off",
    "@typescript-eslint/quotes": [
      "error",
      "single",
      { "allowTemplateLiterals": true }
    ],
    "@typescript-eslint/semi": ["error", "always"],
    "@typescript-eslint/type-annotation-spacing": "error"
  }
}