Kako sam uveo red u projekt gdje je šuma ravnih ruku (postavke tslint, prettier, itd.)

Dobrodošli. Ovdje je Omelnytsky Sergey. Danas ću podijeliti s vama jednu od svojih glavobolja, a to je što učiniti kada na projektu rade različiti programeri, koristeći primjer Angular aplikacije.

Kako sam uveo red u projekt gdje je šuma ravnih ruku (postavke tslint, prettier, itd.)

Nekako se dogodilo da sam dugo radio samo sa svojim timom, gdje smo već dugo dogovarali pravila oblikovanja, komentiranja, margine i slično. Navikli smo se na to i živjeli smo sretno. Na radost, čak sam objavio članak na Habr-u o našem stilu kodiranja. Tako smo iz čarobnog razloga koristili samo tslint na pre-commit.

I tu smo se proširili. Pojavio se novi projekt s nasljeđenim kodom, a uz to nova četvorka sjajnih programera. I nešto nije išlo po planu.

Kako sam uveo red u projekt gdje je šuma ravnih ruku (postavke tslint, prettier, itd.)

Mislim da mnogi znaju da rad s nasljeđenim kodom nije ugodan. U mojoj karijeri dobio sam samo jedan projekt koji me oduševio, a ostalo… O čemu sam to?) Ah da.

Iskreno govoreći, arhitektura projekta ostavljala je mnogo toga za poželjeti, a komentari i tipizacija su nam samo sanjali. U nekom trenutku postao sam pomalo obeshrabren jer naša dokumentacija o pravilima oblikovanja nije funkcionirala, komentari se nisu pisali, tip — što je to?). S tim se nešto moralo učiniti.

Za one kojima nije jasno što odmah učiniti:

  • Podelili smo tslint na meka pravila (za pre-commit) i stroga pravila (za ide, da podsjeti programere na ono što su zaboravili učiniti)

  • Postavili smo automatsku ispravku mogućih pravila strogi tslint na pre-commit

  • Napisali smo pravila za prettier

  • Igrali smo se da pokrenemo ng lint s lint-staged

Prvi korak — dijeli i vladaj

Kada mi je pala na pamet ideja da pooštrimo pravila lintera, pomislio sam da ćemo se obesiti. Kod je nasljeđen. Treba ga shvatiti, a u tom obimu lako se može zagubiti. Donijeli smo odluku da stvorimo drugi linterski alat za ide, koji će nas neprekidno podsjećati i tjerati da pišemo jsdoc za metode i svojstva, da pišemo sučelja ili prokletu onPush i slično.

Dakle, u korijenu imamo dva tslint datoteke:

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": "Koristite cloneDeep (lodash) za kopiranje objekta"
      }
    ],
    "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
  }
}

U datoteci src/tslint zamijenili smo standardni tslint s 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`

Nakon toga smo se počeli mučiti s podvučenim stvarima koje treba ispraviti.

Drugi korak — ispraviti nekoliko stvari

Kako sam uveo red u projekt gdje je šuma ravnih ruku (postavke tslint, prettier, itd.)

tslint ima pravila s ima autoispravljač. Pa dajmo iskoristiti.

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

Ovdje pokrećemo stroga pravila lintersa s automatskom ispravkom dostupnih parametara i kažemo da ova naredba ne vraća greške (naš cilj je svejedno automatska ispravka).

Treći korak — piši lijepo

Kada svatko piše na svoj način, to s vremenom postaje umarajuće. Kod treba pisati tako da izgleda kao da ga piše jedna osoba. Zato sam dodao prettier s sljedećim postavkama:

.prettierr.yaml

printWidth: 200     # Maksimalni broj znakova u retku
tabWidth: 2         # Razmaka u tabu
singleQuote: true   # Koristi jednostruke navodnike
trailingComma: all  # Koristi zareze gdje je moguće
arrowParens: always # Strelične funkcije izgledaju (x) => x
overrides:
  - files: "*.ts"   # Provjera .ts datoteka
    options:
      parser: typescript  # Jezik u .ts datotekama

I dodao sam naredbu: prettier --write --config .prettierr.yaml

Četvrti korak — Kako ćeš ti sve ovo pokrenuti?

Sada ćemo detaljnije razložiti kako sve ovo pokrenuti. Kako bi sve ovo radilo, trebamo preuzeti sljedeće biblioteke:

npm i -D prettier lint-staged husky

Pomoću husky postavit ćemo pokretanje naših naredbi na git hook — pre-commit. lint-staged će pokretati naše naredbe ovisno o izmijenjenim datotekama (također će te datoteke dodati našim naredbama).

Želio bih još odmah opisati problem s kojim sam se susreo. U našem projektu koristimo ng lint. Kada ga koristimo u kombinaciji s lint-staged, dodaju nam izmijenjene datoteke u našu naredbu. Za to ng lint ima opciju --files, ali koliko sam shvatio, ne prepoznaje grupu datoteka, i treba mu dodati ovu opciju za svaku datoteku. Zato sam morao stvoriti datoteku:

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

Za pokretanje ove datoteke moramo proslijediti naziv projekta. On se nalazi u datoteci angular.json u svojstvu project. U mom slučaju to je partner-account i partner-account-e2e. Treba mi prvi.

Vratit ću se na postavke. Naš package.json sada izgleda ovako:

  "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"
    ]
  },

Obratite pažnju na lint-staged --relative. Parametar --relative je obavezan. Sada prilikom commita pokreće se lint-staged. On zauzvrat bira datoteke i pokreće popis naredbi ovisno o njima.

Nažalost, to ne ukida reviziju koda, ali je postao mnogo čišći. Primijetit ću da rjeđe podsjećam programere na pristupne modifikatore, opis metoda i svojstava, a njihova kreativnost se piše u jedinstvenom stilu (pa skoro 😀 ).

P.S. — Hvala našem PM-u za slike.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster