Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

И снова здравствуйте. На связи Омельницкий Сергей. Сегодня я поделюсь с Вами одной из своих головных болей, а именно — что делать, когда проект пишут много разноуровневых программистов на примере angular приложения.

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

Так повелось, что я долгое время работал только со своей командой, где мы уже давно согласовывали правила оформления, комментирования, отступы и т.п. Притерлись к ним и жили дружно и счастливо. На радостях я даже опубликовал статью на Хабр по нашему кодстайлу. Поэтому из чего-то магического мы использовали только tslint на пре-коммит.

И тут мы разрослись. Появился новый проект с унаследованным кодом, а к нему в придачу новые разработчики в размере 4-х добрых молодцев. И чет тут пошло не по плану.

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

Я думаю многие знают, что работа с унаследованным кодом не кайф. На моей памяти я получил только один проект от которого был в восторге, а остальное… Так о чем я?) Ах да.

Откровенно говоря архитектура в проекте оставляла желать лучшего, а комментарии и типизация нам только снилась. В какой-то момент я приуныл от того, что наша документашка по правилам оформления не работает, комментарии не пишутся, тип — что это?). Вот с этим нужно было что-то делать.

Для тех кому не терпится узнать все шаги сразу:

  • Мы разделили tslint на мягкие правила ( для pre-commit ) и жесткие правила ( для ide, чтоб напоминала о том, что разработчики забыли сделать )

  • Повесили на pre-commit автофиксацию возможных правил от жесткого tslint

  • Написали правила для prettier

  • Танцевали с бубном чтоб запустить ng lint с lint-staged

Шаг первый — разделяй и властвуй

Когда мне пришла идея ужесточить правила линтера я подумал, что мы повесимся. Код-то унаследованный. В нем нужно разбираться, а в таком объеме можно закопаться. Было принято решения создать 2-й линтер для ide, которое бы мозолил глаза и заставлял писать jsdoc для методов и св-в, писать интерфейсы или зласчастный onPush и т.п.

Итак в корне у нас начало лежать 2 tslin файла:

tsconfig.json

{
  "rulesDirectory": [
    "node_modules/codelyzer"
  ],
  "rules": {
    "arrow-return-shorthand": true,
    "callable-types": true,
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "curly": true,
    "deprecation": {
      "severity": "warn"
    },
    "eofline": true,
    "forin": true,
    "import-blacklist": [
      true,
      "rxjs/Rx"
    ],
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "interface-over-type-literal": true,
    "label-position": true,
    "max-line-length": [
      true,
      200
    ],
    "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": [
      false,
      "ignore-params"
    ],
    "no-duplicate-imports": true,
    "no-misused-new": true,
    "no-non-null-assertion": true,
    "no-redundant-jsdoc": true,
    "no-shadowed-variable": false,
    "no-string-literal": false,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": [
      true,
      "ignore-comments",
      "ignore-jsdoc"
    ],
    "no-unnecessary-initializer": true,
    "no-unused-expression": true,
    "no-use-before-declare": false,
    "no-var-keyword": true,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "prefer-const": true,
    "quotemark": [
      true,
      "single"
    ],
    "radix": false,
    "semicolon": [
      true,
      "always"
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "unified-signatures": true,
    "variable-name": false,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "directive-selector": [
      true,
      "attribute",
      "app",
      "camelCase"
    ],
    "component-selector": [
      true,
      "element",
      "app",
      "kebab-case"
    ],
    "no-output-on-prefix": false,
    "no-inputs-metadata-property": true,
    "no-outputs-metadata-property": true,
    "no-host-metadata-property": true,
    "no-input-rename": false,
    "no-output-rename": true,
    "use-lifecycle-interface": true,
    "use-pipe-transform-interface": true,
    "component-class-suffix": true,
    "directive-class-suffix": true,
    "no-consecutive-blank-lines": true
  }
}

tslint.ide_only.json

{
  "rulesDirectory": [
    "node_modules/codelyzer"
  ],
  "rules": {
    "completed-docs": [
      true,
      {
        "properties": true,
        "methods": true
      }
    ],
    "no-angle-bracket-type-assertion": true,
    "no-any": true,
    "prefer-output-readonly": true,
    "prefer-on-push-component-change-detection": true,
    "array-type": [
      true,
      "array"
    ],
    "typedef": [
      true,
      "call-signature",
      "arrow-call-signature"
    ],
    "arrow-return-shorthand": true,
    "callable-types": true,
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "curly": true,
    "deprecation": {
      "severity": "warn"
    },
    "eofline": true,
    "forin": true,
    "import-blacklist": [
      true,
      "rxjs/Rx"
    ],
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "interface-over-type-literal": true,
    "label-position": true,
    "max-line-length": [
      true,
      200
    ],
    "member-access": [
      true,
      "check-parameter-property",
      "check-accessor"
    ],
    "member-ordering": [
      true,
      {
        "order": [
          "public-static-field",
          "protected-static-field",
          "private-static-field",
          "public-instance-field",
          "protected-instance-field",
          "private-instance-field",
          "constructor",
          "public-static-method",
          "protected-static-method",
          "private-static-method",
          "public-instance-method",
          "protected-instance-method",
          "private-instance-method"
        ]
      }
    ],
    "no-arg": true,
    "no-bitwise": true,
    "no-console": true,
    "no-construct": true,
    "no-debugger": true,
    "no-duplicate-super": true,
    "no-empty": false,
    "no-empty-interface": true,
    "no-duplicate-switch-case": true,
    "no-eval": true,
    "no-inferrable-types": [
      false,
      "ignore-params"
    ],
    "no-duplicate-imports": true,
    "one-variable-per-declaration": true,
    "no-misused-new": true,
    "no-non-null-assertion": true,
    "prefer-template": [
      true,
      "allow-single-concat"
    ],
    "ordered-imports": true,
    "no-redundant-jsdoc": true,
    "no-shadowed-variable": false,
    "no-string-literal": false,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": [
      true,
      "ignore-comments",
      "ignore-jsdoc"
    ],
    "ban": [
      true,
      {
        "name": [
          "Object",
          "assign"
        ],
        "message": "Используйте cloneDeep (lodash) для копирования объекта"
      }
    ],
    "max-classes-per-file": [
      true,
      1
    ],
    "cyclomatic-complexity": [
      true,
      6
    ],
    "static-this": true,
    "no-unnecessary-initializer": true,
    "no-unused-expression": true,
    "no-var-keyword": true,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "prefer-const": true,
    "quotemark": [
      true,
      "single"
    ],
    "radix": false,
    "semicolon": [
      true,
      "always"
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "unified-signatures": true,
    "variable-name": false,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "directive-selector": [
      true,
      "attribute",
      "app",
      "camelCase"
    ],
    "component-selector": [
      true,
      "element",
      "app",
      "kebab-case"
    ],
    "no-output-on-prefix": false,
    "no-inputs-metadata-property": true,
    "no-outputs-metadata-property": true,
    "no-host-metadata-property": true,
    "no-input-rename": false,
    "no-output-rename": true,
    "use-lifecycle-interface": true,
    "use-pipe-transform-interface": true,
    "component-class-suffix": true,
    "directive-class-suffix": true,
    "no-consecutive-blank-lines": true
  }
}

Nel file src/tslint мы заменили стандартный tslint на ide

src/tslint.json

{
    "extends": "../tslint.ide_only.json",
    "rules": {
        "directive-selector": [
            true,
            "attribute",
            "app",
            "camelCase"
        ],
        "component-selector": [
            true,
            "element",
            "app",
            "kebab-case"
        ]
    }
}

И поправил запуск нашего линтера в скритах package.json

ng lint --tslint-config ./tslint.json --fix`

После чего мы стали вешаться от подчеркнутых вещах, которые нужно править.

Шаг второй — поправить пару моментов

Come ho fatto ordine nel progetto, dove c'era una selva di configurazioni (impostazioni tslint, prettier, ecc.)

У tslint есть правила с has fixer. Так давай воспользуемся.

tslint --project tslint.ide_only.json --fix --force

Здесь мы запускаем правила жесткого линтера с автофиксацией доступных параметров и говорим, чтобы эта команда не возвращала ошибок ( тут наша цель все-таки делать автоисправление ).

Шаг третий — пиши красиво

Когда каждый пишет в своей манере это в конечном счете утомляет. Код нужно писать так, чтоб казалось, что это делает один человек. Для этого я прикрутил prettier, со следующими настройками:

.prettierr.yaml

printWidth: 200     # Максимальное кол-во символов в строке
tabWidth: 2         # Пробелов в Табе
singleQuote: true   # Использовать одинарные кавычки
trailingComma: all  # Использовать запятые где возможно
arrowParens: always # Стрелочные ф-ии выглядят (x) => x
overrides:
  - files: "*.ts"   # Проверка файлов *.ts
    options:
      parser: typescript  # Язык в файлах *.ts

И добавил команду: prettier --write --config .prettierr.yaml

Шаг четвертый — И как ты прикажешь все это запускать?

Давайте теперь подробнее разберем как же все это запускать. Для того, чтоб это все работало нам нужно скачать следующие либы:

npm i -D prettier lint-staged husky

С помощью husky мы повесим запуск наших команд на git хук — pre-commit. lint-staged будет запускать нам команды в зависимости от измененных файлов ( так же подставлять эти файлы к нам в команды).

Хотелось бы еще сразу обрисовать проблему, с которой столкнулся я. У нас в проекте мы используем ng lint. Когда мы используем его в связке с lint-staged, то в нашу команду добавляются измененные файлы. У ng lint есть для этого ключ --files, но, как я понял, он не видит пачку файлов, и ему нужно на каждый файл добавлять этот ключ. Для этого мне пришлось создать файл:

lint.sh

#!/bin/bash

PROJECT=$1
shift
SOURCES=$@
DESTINATIONS=""
DELIMITER=""

for src in $SOURCES
do
    DELIMITER=" --files "
    DESTINATIONS="$DESTINATIONS$DELIMITER${src}"
done

ng lint $PROJECT --tslint-config ./tslint.json $DESTINATIONS

Для запуска этого файла мы должны передать название проекта. Оно находится в файле angular.json в свойстве project. В моем случае это partner-account e partner-account-e2e. Мне нужен 1-й.

Вернусь к настройке. Наш package.json теперь выглядит так:

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged --relative"
    }
  },
  "lint-staged": {
    "*.{ts,js}": [
      "prettier --write --config .prettierr.yaml",
      "tslint --project tslint.ide_only.json --fix --force",
      "sh lint.sh partner-account",
      "git add"
    ],
    "*.{html,scss,css}": [
      "prettier --write --config .prettierr.yaml",
      "git add"
    ]
  },

Fai attenzione a lint-staged --relative. Il parametro --relative там обязателен. Теперь при коммите у нас запускается lint-staged. Он в свою очередь отбирает файлы и запускает в зависимости он них список команд.

К сожалению это не отменяет ревью кода, но он стал гораздо чище. Замечу, что я реже стал напоминать разработчикам про модификаторы доступа, описание методов и св-в, а их творчество стало написано в едином стиле ( ну почти 😀 ).

P.S. — Спасибо за картинки нашему PM.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster