Jak zabezpieczyć projekt przed przypadkowymi błędami przy użyciu narzędzi ESLint, Prettier i Husky

Każdy programista, niezależnie od poziomu zaawansowania i lat doświadczenia, może mieć gorszy dzień i przez przypadek wprowadzić zmiany, które powodować będą bugi lub po prostu nie będą wpisywały się w dobre praktyki wytwarzania kodu.  Na szczęście jest kilka sposobów, dzięki którym możemy zabezpieczyć nasz JS-owy projekt przed takimi przypadkami. 

Jakub Krymarys

Jakub Krymarys. W IT aktywny zawodowo od 4 lat. Obecnie związany z STX Next, gdzie realizuje się jako JavaScript Developer ze specjalizacją w React.js. Miłośnik nowinek ze świata IT oraz wszelkich technologicznych gadżetów. Uwielbia optymalizować swoją pracę. 

 

 

 

 


Zakładam, że pierwszą rzeczą jaka przychodzi wam na myśl są różnego rodzaju testy. Oczywiście są one jak najbardziej dobrą metodą, ale w tym artykule zajmiemy się czymś innym. Zamiast skupiać się na testowaniu funkcjonalności aplikacji, skupimy się na samym kodzie. Wykorzystamy do tego:

  • ESlint  – analizuje kod JS ,aby znaleźć potencjalne błędy i złe praktyki,
  • Prettier  – formatuje kod zgodnie z przyjętym standardem,
  • Husky – pozwala na integrację z Git Hookami, co umożliwia zautomatyzowanie dwóch powyższych narzędzi.

Wszystkie te narzędzia dobrze współpracują z każdym projektem, który bazuje na Node.js. Z uwagi na to, że chciałbym podać też konkretne przykłady configów, te będą omówione na przykładzie “czystego” projektu React.js stworzonego przy pomocy Create React App (CRA).

ESlint – analiza kodu

Zacznijmy od ESlint. Jest to tzw linter, czyli narzędzie, które poddaje JavaScriptowy kod statycznej analizie w celu znalezienia wszelkich potencjalnych problemów. Na każdy z nich może on zareagować na dwa różne sposoby – albo oznaczyć go jako warning (i wyświetlać w konsoli stosowny komunikat), albo jako error (w tym przypadku oprócz komunikatu dodatkowo kompilacja kodu zakończy się błędem).

Jeśli pracowaliście z Reactem, to pewnie widzieliście już niejeden warning lub błąd w konsoli przeglądarki. Część z nich to właśnie efekt działania ESLint. Jest on zintegrowany z aplikacją, którą tworzymy za pomocą CRA. Posiada on tam jednak bardzo minimalistyczną konfigurację.  

Tak wygląda bazowy config ESLinta w  pliku package.json aplikacji React.js stworzonej za pomocą CRA:

{
  ()  „eslintConfig”: {
    „extends”: [
      „react-app”,
      „react-app/jest”
    ]
  },  (…)}

Jeżeli jednak z jakiegoś powodu nie macie ESLint w projekcie to można go łatwo dodać za pomocą komendy npm install eslint –save-dev  (https://eslint.org/docs/user-guide/getting-started).

Aby linter był prawdziwym “lifesaverem” naszego projektu, musimy tę bazową konfigurację nieco rozszerzyć. Domyślnie posiada ona tylko zestaw podstawowych zasad specyficznych dla samego Reacta, nie sprawdza natomiast samej składni JS-owej.

Proponuję zacząć od konfiguracji zalecanej przez zespół ESLint –  „eslint:recommended”.  Co dokładnie znajduje się w tym zestawie, można zobaczyć w https://eslint.org/docs/rules/) .

Jak rozszerzyć konfigurację ESLint? 

Konfigurację lintera możemy rozszerzyć na dwa sposoby – poprzez użycie odpowiedniego pola eslintConfig w package.json lub poprzez stworzenie specjalnego pliku konfiguracyjnego w głównym folderze projektu – .eslintrc. Obydwa działają równie dobrze, jednak ja, jako fan rozbijania wszystkiego na jak najmniejsze fragmenty, polecam wydzielić config do nowego pliku.

Pamiętajcie jednak, że gdy stworzycie konfigurację w osobnym pliku, należy usunąć pole eslintConfig z package.json.

Plik konfiguracyjny .eslintrc składa się z kilku sekcji:

{
    „extends”: [()],      // Jakie konfiguracje chcemy rozszerzyć
    „rules”: { () },      // Dodatkowe reguły lub zmiana znaczenia obecnych
    „overrides”: [           // Nadpisanie zasad dla konkretnych plików/grup plików
      {
        „files”: [()],    // które definiujemy tutaj. Np. wszystkie pliki TS
        „rules”: { () }   // same reguły nadpisujemy tutaj
      }
    ]

Nasza podstawowa konfiguracja powinna wyglądać więc mniej więcej tak:

{
    „extends”: [       „eslint:recommended”,
      „react-app”,
      „react-app/jest”
    ]
}

! UWAGA ! Bardzo ważne jest to, żeby „react-app”„react-app/jest” pozostały w „extends” naszego projektu (ponieważ “sprawdzają” mechanizmy Reacta)! 

Jest to dobry punkt wyjścia do porządnego i świadomego używania lintera. Możecie jednak zmienić tę konfigurację (korzystając z oficjalnej dokumentacji) lub po prostu wprowadzić swoje modyfikacje zasad (również dobrze opisane w dokumentacji ESLint).

Kiedy dodać swoje reguły do ESLint? 

Na pewno nie od razu. Sugerowałbym zacząć z rekomendowanym zestawem reguł, a ewentualne zmiany wprowadzać dopiero, gdy jakiejś będzie brakować lub któraś z nich będzie sprzeczna z wymaganiami waszego projektu. Oczywiście, nie zapomnijcie o tym, żeby dobrze przedyskutować to w zespole, aby cały zespół był zgodny i rozumiał, dlaczego taka zasada została zmieniona. 

Żeby dodać swoją zasadę lub zmienić działanie już istniejącej, musimy najpierw znaleźć ją w zbiorze reguł.

Następnie możemy dodać ją do sekcji rules configu (gdy chcemy, żeby odnosiła się do całego projektu) lub do sekcji overrides (jeśli ma działać tylko z pewną grupą plików) z jedną z trzech oczekiwanych wartości podanych niżej, która określać będzie to, jak linter będzie reagował na fragmenty kodu podpadające pod nią:

  • 0 lub “off” – wyłączy zasadę 
  • 1 lub “warn” – linter będzie reagował warningiem 
  • 2 lub “error” – linter będzie reagował wyrzuceniem błędu i przerwaniem kompilacji  

Dla przykładu: „no-console”: „error” będzie blokował kompilacje aplikacji (wyrzucał error), gdy tylko linter wykryje console.log.

Tak wygląda przykładowa konfiguracja rozszerzona o zasadę „no-console”:

{
  „eslintConfig”: {
    „extends”: [
      „react-app”,
      „react-app/jest”,
      „eslint:recommended”
    ],
    „rules”: {
    „no-console”: „off”
  }

Jak uruchamiać Lintera?

Lintera w naszym projekcie możemy uruchomić na kilka sposobów. 

Gdy tylko zrestartujecie aplikację, nowa konfiguracja powinna być już brana pod uwagę i przy każdej kompilacji sprawdzać kod według niej. 

Oczywiście możemy też przeanalizować cały projekt samodzielnie. Jest na to kilka sposobów. Najprostszy z nich to dodanie odpowiedniego skryptu do pliku package.json, a następnie uruchomienie za pomocą komendy nam run lint:

{
  ()
  „scripts”: {
  ()
  „lint”: „eslint –fix ‚./src/**/*.{js,jsx}'”
  }
  ()
}

Lub poprzez narzędzie npx:

npx eslint –fix ‚./src/**/*.{js,jsx}’

Jak pewnie zauważyliście, do komendy eslint dodałem flagę –fix.  Dzięki niej linter automatycznie naprawi cześć napotkanych błędów, co dodatkowo usprawni nam cały proces. 

Prettier – formatowanie kodu

Kolejną rzeczą, którą warto zapewnić w projekcie, jest automatyczne formatowanie kodu według centralnej konfiguracji. Zwykle każdy z programistów w zespole ma trochę inne preferencje (i to jest jak najbardziej ok!), jednak może doprowadzić to do mniejszych lub większych problemów.

Swoją drogą, Prettier powstał jako sposób na zaprzestanie wszystkich dyskusji o tym, który sposób formatowania jest lepszy. Jego styl formatowania jest dość łagodny, ponieważ ma być kompromisem pomiędzy wymaganiami wszystkich deweloperów. 

Jednym z nich na pewno będzie chociażby zamieszanie w pull/merge requestach. Nagle może okazać się, że “zmodyfikowaliśmy” znacznie więcej linii kodu, niż miało to pierwotnie wynikać z samych zmian powiązanych z nową funkcjonalnością czy poprawkami. To tylko nasz formatter uporządkował kod “po swojemu”. Nie zmienia to oczywiście funkcjonalności aplikacji, jednak wprowadza niepotrzebne zamieszanie. Dla osoby przeprowadzającej code review nie od razu będzie jasne, które fragmenty kodu musi sprawdzić.

Żeby wprowadzić ustandaryzowane formatowanie kodu na poziomie projektu wykorzystamy narzędzie Prettier.

Przejdźmy więc do jego samej instalacji. W przeciwieństwie do ESlint, tego narzędzia nie mamy wbudowanego w CRA. Jednak, jak z wszystkimi NPMowymi paczkami, instalacja jest bardzo prosta i ogranicza się do komendy: 

npm install –save-dev prettier

Następnie skonfigurujemy naszego formatera. Posłużą nam do tego dwa pliki .prettierrc.json, który zawiera konfigurację oraz .prettierignore,  gdzie możemy wypisać pliki i foldery, które Prettier ma pominąć (ten plik działa na takiej samej zasadzie jak .gitignore).

Przykładowa konfiguracja .prettierrc.json:

{
„singleQuote”: true,
„trailingComma”: „es5”,
„printWidth”: 120
}

Przykładowa konfiguracja .prettierignore dla Reacta:

node_modules
build

Jeśli dodajecie Prettiera do istniejącego projektu, zauważcie, że pierwszy raz, kiedy go użyjecie, prawdopodobnie sformatuje on większość plików w projekcie. Dlatego też warto zrobić to od razu, w specjalnie przeznaczonym do tego commicie. Pamiętajcie tylko wtedy, aby powiadomić cały zespół o potrzebie ściągnięcia najnowszej wersji kodu. W przeciwnym wypadku grozić wam będą konflikty pomiędzy kodem z nową konfiguracją a niezaktualizowanymi wersjami projektu.

Podobnie jak z linterem, Prettiera możemy uruchomić na dwa sposoby:

  1. Poprzez skrypt w package.json, który uruchomimy za pomocą  npm run prettier:
{
  ()
    „scripts”: {
    „prettier” : „prettier –write .”
    }
  ()
}

2. Lub używając narzędzia npx:

npx prettier –write .

Koniecznie musimy również dostosować konfigurację ESLint, dodając informację, że w projekcie będziemy używać również Prettiera. W przeciwnym wypadku oba systemy mogą ze sobą kolidować. 

Aby to zrobić, należy najpierw zainstalować ESLintowy zestaw zasad dostosowany do Prettiera za pomocą komendy:

npm install –save-dev eslint-config-prettier

A następnie należy dodać config prettier do “extendsów” w pliku .eslintrc. Bardzo ważne jest, żeby dodać go jako ostatnią pozycję, ponieważ musi on nadpisać kilka pozycji z wcześniejszych zestawów zasad. 

  ()
  „eslintConfig”: {
      „extends”: [
          „eslint:recommended”,
          „react-app”,
          „react-app/jest”,
          „prettier”
      ],
  ()
  }

Husky – automatyzacja 

Na koniec zautomatyzujmy uruchamianie obu tych narzędzi, aby usprawnić nasz workflow. Użyjemy do tego Husky’iego. Jest to narzędzie pozwalające na integrację z Git Hookami… tylko tyle i aż tyle! 

Git Hooki to sposób na uruchomienie dowolnych skryptów w reakcji na różne akcje związane z systemem kontroli wersji git. 

Aby zrobić to w miarę prosty sposób, użyjemy projektu lint-staged, który usprawni ten proces oraz wprowadzi jeszcze jedną istotną optymalizację.

Przy okazji czytania akapitów poświęconych ESlint i Prettier mogło się zdarzyć, że zaczęliście wątpić w to, czy takie rozwiązanie przypadkiem nie spowolni Waszego projektu. W końcu ciągłe formatowanie i analizowanie kilkuset (albo i kilku tysięcy!) linii kodu w kilkunastu plikach zdecydowanie może zajmować dłuższą chwilę, co przy każdym commicie może budzić irytację.    

Co więcej, łatwo jest zauważyć, że większość tych plików nawet nie będzie modyfikowana, więc ciągłe ich analizowanie będzie po prostu stratą czasu. 

Na szczęście i na to jest sposób – narzędzie lint-staged. Pozwala ono na bajecznie prostą integrację z Git Hookiem pre-commit, co jest zupełnie wystarczające na początek. 

Instalujemy go w trochę inny sposób niż resztę. Tym razem wykorzystamy komendę:

npx mrm@2 lint-staged

W celu doczytania, jak dokładnie to narzędzie działa zachęcam do przejrzenia githubowej strony projektu.

Ta komenda (czy właściwie skrypt, który uruchamiamy za jej pomocą) robi kilka istotnych dla nas rzeczy: 

  • zainstaluje Husky’iego
  • zainstaluje lint-staged,
  • skonfiguruje lint-staged na podstawie tego czy mamy już zainstalowanego ESlint oraz Prettier.

Po zainstalowaniu lint-staged musimy dodać do package.json konfigurację tego narzędzia. Składa się ona z JSONa, który jako klucz przyjmuje nazwę pliku (lub regex określający grupę plików), a jako wartość string z komendą do wykonania lub tablicę stringów jeśli takich komend mamy kilka.

Jeśli stworzyliście Waszą aplikację przez CRA lint-staged, najprawdopodniej skonfigurował Wam tylko Prettiera. Dlatego też rozszerzymy sobie naszą konfigurację o lintera, tak jak na przykładzie poniżej:

{
  ()
  „lint-staged”: {
    „*.{js,jsx}”: „eslint –fix src/”,
    „*.{js,jsx,json,css,md}”: „prettier –write”
  }
  ()
}

Zwróćcie uwagę na to, jakie pliki ma obsłużyć jedno i drugie narzędzie. ESLint współpracuje tylko z JavaScriptem, podczas gdy Prettier z wieloma innymi formatami. 

Podsumowanie 

Kilkanaście minut poświęconych na wyżej przedstawioną konfigurację pozwoli zaoszczędzić wiele nerwów i niezliczonych godzin, które spędzilibyście na debugowaniu problemu, jaki mógł zostać wyłapany przez lintera już na etapie pisania kodu. Do tego mnóstwo czasu poświęconego na analizowanie zmian w gicie, wynikających tylko z różnic konfiguracji formatera w IDE u poszczególnych programistów z zespołu. Zmian, które nie mają wpływu na funkcjonalność aplikacji, a są jedynie formatowaniem kodu.

Oprócz tego wasz kod będzie po prostu ładniejszy i zgodny z dobrymi praktykami, przez co na pewno będzie się łatwiej z nim pracować. 

W dodatku głębsze zrozumienie tego jak działa ESLint i dlaczego pewne konstrukcje są przez niego oznaczane jako błąd, doprowadzi do lepszego zrozumienia JavaScriptu i  zapozna Was z dobrymi zasadami, których warto się trzymać pisząc projekty w tym szalonym języku. 

Jak pewnie się domyślacie, to, co poruszyłem w tym artykule to dopiero wierzchołek góry lodowej. Zwłaszcza w kontekście samego ESLint i tego jakie możliwości oferuje to narzędzie. Poniżej zostawiam wam kilka ciekawych linków, które pozwolą bardziej zgłębić wiedzę w tym temacie:

Oraz strony wykorzystanych tutaj projektów:

ESLint ESLint – Pluggable JavaScripts linter

Zdjęcie główne pochodzi z unsplash.com.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
daria hernas hyland
Ludzie ponad procesy, rezultaty ponad przywiązanie do biurka. O kształtowaniu employee experience w Hyland