Dobrodošli. Ovdje je . 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.

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 . 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.

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

tslint ima pravila s ima autoispravljač. Pa dajmo iskoristiti.
tslint --project tslint.ide_only.json --fix --forceOvdje 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 datotekamaI 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 huskyPomoć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 $DESTINATIONSZa 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
